Fix: Authorise @github2gerrit comment directives - #389
Merged
tykeal merged 1 commit intoAug 25, 2026
Merged
Conversation
ModeSevenIndustrialSolutions
requested review from
a team
and
a balanced review from Copilot
August 25, 2026 10:00
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 25, 2026 10:00
View session
There was a problem hiding this comment.
Pull request overview
Secures PR comment directives by authorizing trusted GitHub associations before command execution.
Changes:
- Adds configurable association-based trust rules.
- Filters and logs untrusted comment directives.
- Adds documentation and authorization tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/github2gerrit/trust.py |
Defines shared trust policy. |
src/github2gerrit/github_api.py |
Filters comments by author association. |
src/github2gerrit/core.py |
Enforces authorization before commands. |
src/github2gerrit/config.py |
Registers the trust configuration key. |
tests/test_pr_command_authorisation.py |
Tests trust and command gating. |
tests/test_pr_commands.py |
Updates existing command fixtures. |
docs/features.md |
Documents command authorization. |
docs/cli.md |
Documents the environment variable. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ModeSevenIndustrialSolutions
force-pushed
the
fix/pr-command-authorisation
branch
from
August 25, 2026 10:13
fc88911 to
9930560
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 25, 2026 10:14
View session
The PR command system read comment bodies and discarded their authors, so on a public mirror any GitHub user able to leave a comment could direct the tool. Today that means forcing creation of a Gerrit change the UPDATE path deliberately declined to make. The blast radius is small because one command is registered, but pr_commands is an extensible registry and every command added would have inherited no authorisation at all. Close it before the second command rather than after. Gate recognition on the comment author's author_association. That field already arrives on the payloads being fetched, so it costs no extra API call and no extra token scope. Trust OWNER, MEMBER and COLLABORATOR by default; CONTRIBUTOR means only that the author had a pull request merged once, which any outside contributor can achieve. Trust lives in one module because the fork approval gate will need the same rule, and two definitions would drift. Absent, empty or malformed values are untrusted, so a missing signal never grants authority. Report refusals: ignored directives log the author and association at warning level, otherwise a maintainer whose command is declined has no way to tell it was read at all. Deciding what counts as a directive stays with the command system, which exposes contains_directive and passes it in, so the grammar cannot drift from the parser's. A bare mention, or one followed only by whitespace, is not a directive and so does not warn on every scan. Make the authorised path the only obvious one. pr_directives composes fetch, authorise and parse, and is documented as the entry point for the command registry. pr_commands stays network-free and renames its inputs to trusted_comment_bodies, so a caller passing raw API comments has to ignore the name, the docstring warning and the module warning to reintroduce the defect. This is what issue 382 meant by new registry commands inheriting the gate; the parser cannot enforce it alone without reaching the network. get_trusted_comment_bodies also routes through the decorated _get_issue, so this path gains the retry and backoff that every other comment reader already had and it had bypassed. Two existing tests set no author_association and so encoded the old any-comment-counts behaviour; they now use a trusted author. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
ModeSevenIndustrialSolutions
force-pushed
the
fix/pr-command-authorisation
branch
from
August 25, 2026 10:29
9930560 to
411afac
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
August 25, 2026 10:30
View session
tykeal
approved these changes
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #382
Closes the one issue from the fork-handling audit that is exploitable today,
on every public mirror using this action — automation-only or not, since comment
directives are not fork-scoped.
The problem
Orchestrator._should_create_missingread comment bodies and threw the authorsaway:
comment.userwas fetched by the API and discarded. Greppingsrc/andtests/forauthor_association,get_collaborator_permissionorhas_in_collaboratorsreturned nothing — there was no authorisation anywhere.So on a public repository, any GitHub user able to leave a comment could post
@github2gerrit create missing changeand force creation of a Gerrit changethat the UPDATE path deliberately declined to make.
The immediate blast radius is small, because exactly one command is registered.
The structural problem is the reason to fix it now:
pr_commands.pyis anextensible registry with a commented-out
force resubmitexample, and everycommand added would have inherited zero authorisation by construction.
The fix
Gate command recognition on the comment author's
author_association.Why that signal
It already arrives on the payloads being fetched, so it costs no extra API call
and no extra token scope. The alternative — querying collaborator permission —
costs a request per user and, on these repositories, asks the wrong question:
opendaylight/mdsal's collaborator list is entirely LF infrastructure staff andbots, while project committers appear only as organisation
MEMBERs.Default trusted set is
OWNER,MEMBER,COLLABORATOR.CONTRIBUTORis deliberately excluded. It means only that the author has had apull request merged at some point, which any outside contributor can achieve and
which conveys no authority.
Overridable through
G2G_TRUSTED_ASSOCIATIONS(also added toconfig.KNOWN_KEYS, so per-organization files can set it). A blank value keepsthe default rather than trusting nobody or everybody.
One definition of trust
src/github2gerrit/trust.pyis new and deliberately small. The fork approvalgate (#383) needs the same rule, and two copies would drift. Absent, empty or
malformed values are untrusted, so a missing signal never grants authority —
matching
head_repo_is_trustedfrom #384.The authorised path is the obvious one
Gating only the existing caller would fix today's defect and leave command
number two free to reintroduce it. So the three steps that must always happen
together now live behind one door,
src/github2gerrit/pr_directives.py:pr_commandsrenames its inputs totrusted_comment_bodiesand carries anexplicit "this module performs no authorisation" warning, replacing the previous
"no other code changes are required" note that invited the mistake.
This is enforced by naming, documentation and tests rather than by the type
system:
parse_commandsstays callable with any list, because forcing itthrough a network-touching API would make the parser untestable in isolation.
Refusals are reported
pr_directiveslogs ignored directives once, for every consumer:Silently dropping a maintainer's command would leave them with no way to tell it
had been read at all.
Directive detection uses the parser's own grammar via
contains_directive, so abare
@github2gerritmention — whichparse_commandstreats as neither a matchnor an unrecognised directive — does not produce a warning on every scan. That
also caught a latent quirk:
_MENTION_RE's.matches spaces, so@github2gerritsatisfied the regex with a whitespace-only capture.Incidental fix
The old code called
pr_obj.as_issue()directly, bypassing the decorated_get_issue. That path therefore had none of the retry, backoff or metrics everyother comment reader gets. It now goes through
github_api, so it does.Compatibility
Nothing bot-driven posts
@github2gerritdirectives — the command exists for amaintainer to unstick a failed change — so this cannot break an automation flow.
A maintainer on an automation-only repository will be
OWNER,MEMBERorCOLLABORATOR.Two existing tests in
tests/test_pr_commands.pyused bareMagicMockcommentswith no
author_association, and so encoded the old any-comment-countsbehaviour. They now set a trusted author, which is what they were really testing.
Validation
uv run pytest tests/— full suite passes, coverage 71.80%tests/test_pr_command_authorisation.pycovers the trust rule, environmentoverride and normalisation, comment partitioning, bare and whitespace-only
mentions, the composed entry point, and end-to-end gating — including that an
outsider cannot launder a directive by posting it alongside a trusted comment,
and that refusals are logged exactly once
TestRegistryDocumentsTheGateasserts the parameter naming and module warning,so the contract cannot rot silently
prek run --all-files,aislop(score 100, zero findings, unchanged from base)Follow-up
trust.py