Skip to content

Fix: Authorise @github2gerrit comment directives - #389

Merged
tykeal merged 1 commit into
lfreleng-actions:mainfrom
modeseven-lfreleng-actions:fix/pr-command-authorisation
Aug 25, 2026
Merged

Fix: Authorise @github2gerrit comment directives#389
tykeal merged 1 commit into
lfreleng-actions:mainfrom
modeseven-lfreleng-actions:fix/pr-command-authorisation

Conversation

@ModeSevenIndustrialSolutions

@ModeSevenIndustrialSolutions ModeSevenIndustrialSolutions commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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_missing read comment bodies and threw the authors
away:

comment_bodies = [c.body or "" for c in issue.get_comments()]

comment.user was fetched by the API and discarded. Grepping src/ and
tests/ for author_association, get_collaborator_permission or
has_in_collaborators returned nothing — there was no authorisation anywhere.

So on a public repository, any GitHub user able to leave a comment could post
@github2gerrit create missing change and force creation of a Gerrit change
that 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.py is an
extensible registry with a commented-out force resubmit example, and every
command 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 and
bots, while project committers appear only as organisation MEMBERs.

Default trusted set is OWNER, MEMBER, COLLABORATOR.

CONTRIBUTOR is deliberately excluded. It means only that the author has had a
pull request merged at some point, which any outside contributor can achieve and
which conveys no authority.

Overridable through G2G_TRUSTED_ASSOCIATIONS (also added to
config.KNOWN_KEYS, so per-organization files can set it). A blank value keeps
the default rather than trusting nobody or everybody.

One definition of trust

src/github2gerrit/trust.py is new and deliberately small. The fork approval
gate (#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_trusted from #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:

github_api      fetch comments, partition by author trust  (no grammar)
pr_commands     parse text for commands                    (no network)
pr_directives   compose the two                            (the entry point)

pr_commands renames its inputs to trusted_comment_bodies and carries an
explicit "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_commands stays callable with any list, because forcing it
through a network-touching API would make the parser untestable in isolation.

Refusals are reported

pr_directives logs ignored directives once, for every consumer:

🚫 Ignoring @github2gerrit directive(s) from untrusted comment author(s):
   outsider (NONE). Trusted associations: COLLABORATOR, MEMBER, OWNER

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 a
bare @github2gerrit mention — which parse_commands treats as neither a match
nor an unrecognised directive — does not produce a warning on every scan. That
also caught a latent quirk: _MENTION_RE's . matches spaces, so
@github2gerrit satisfied 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 every
other comment reader gets. It now goes through github_api, so it does.

Compatibility

Nothing bot-driven posts @github2gerrit directives — the command exists for a
maintainer to unstick a failed change — so this cannot break an automation flow.
A maintainer on an automation-only repository will be OWNER, MEMBER or
COLLABORATOR.

Two existing tests in tests/test_pr_commands.py used bare MagicMock comments
with no author_association, and so encoded the old any-comment-counts
behaviour. 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.py covers the trust rule, environment
    override 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
  • TestRegistryDocumentsTheGate asserts 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

@ModeSevenIndustrialSolutions
ModeSevenIndustrialSolutions requested review from a team and a balanced review from Copilot August 25, 2026 10:00
@github-actions github-actions Bot added the bug Something isn't working label Aug 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/github2gerrit/github_api.py Outdated
Copilot AI review requested due to automatic review settings August 25, 2026 10:13

This comment was marked as resolved.

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>
Copilot AI review requested due to automatic review settings August 25, 2026 10:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

@tykeal
tykeal merged commit a485b30 into lfreleng-actions:main Aug 25, 2026
28 checks passed
@ModeSevenIndustrialSolutions
ModeSevenIndustrialSolutions deleted the fix/pr-command-authorisation branch August 25, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR comment commands accept directives from any user

3 participants