Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/actions/authorize-commenter/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Authorize commenter
description: >-
Check that the author of a comment has write access to the repository, and
fail if they do not.

Workflows triggered by `issue_comment` run from the default branch with
access to secrets, so gating them on the comment body alone lets anyone who
can comment start them. Run this action as the first job of such a workflow
and give every privileged job `needs` on that job. A failed dependency skips
the jobs that need it, so there is no condition to forget.

This authorizes the person who wrote the comment, not the code they wrote it
on. A workflow that passes this gate and then checks out the pull request
head or merge ref runs untrusted code with the privileges the gate was
protecting.

The default `GITHUB_TOKEN` can read collaborator permissions with no more
than `contents: read`. If a token ever cannot, this action fails, so an
underpowered token denies everyone rather than quietly authorizing them.

inputs:
username:
description: The login to authorize. Defaults to the author of the comment
that triggered the workflow.
required: true
default: ${{ github.event.comment.user.login }}

github-token:
description: The GitHub token to use for authentication. Defaults to the
standard GITHUB_TOKEN.
required: true
default: ${{ github.token }}

runs:
using: composite
steps:
- name: Check the commenter's repository permission
uses: actions/github-script@v9
env:
USERNAME: ${{ inputs.username }}
with:
github-token: ${{ inputs.github-token }}
script: |
const { USERNAME } = process.env;

if (!USERNAME) {
return core.setFailed('No username to authorize. Pass `username`, or run this action on an event that has a comment author.');
}

// `permission` collapses custom roles onto admin, write, read and
// none, so admin and write are exactly the logins that can push.
// Everyone else reads back as `read` on a public repository and
// `none` on a private one. A login that no longer resolves, such as
// `ghost`, raises instead, which fails this step rather than
// authorizing.
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: USERNAME,
});

if (!['admin', 'write'].includes(data.permission)) {
return core.setFailed(`@${USERNAME} has ${data.permission} permission on this repository, which is not enough to run this command.`);
}

core.info(`@${USERNAME} has ${data.permission} permission.`);
37 changes: 37 additions & 0 deletions .github/workflows/test-authorize-commenter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test "Authorize Commenter" Action

# Unlike the other test-*.yml workflows, this one triggers directly rather than
# through workflow_call, so that it actually runs on every pull request.

on: pull_request

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

# Skipped on forks, where the author has read access and would be refused.
- name: A login with write access is authorized
if: github.event.pull_request.head.repo.fork == false
uses: ./.github/actions/authorize-commenter
with:
username: ${{ github.actor }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bot PRs fail write-access test

Low Severity

The write-access step treats github.actor as a collaborator with push rights on every non-fork pull request. Same-repository bot PRs, including Dependabot, usually get none from the permission API, so that step fails and the new check goes red.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d30ca1c. Configure here.


- name: A login without write access is refused
id: refused
continue-on-error: true
uses: ./.github/actions/authorize-commenter
with:
username: octocat

- name: Fail if the refusal did not happen
if: steps.refused.outcome != 'failure'
run: |
echo "Expected octocat to be refused, but the action succeeded."
exit 1
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `authorize-commenter` action, which fails unless the author of a comment has write access to the repository, so that `issue_comment` triggered workflows can refuse to run privileged jobs for someone who only has read access ([#286](https://github.com/MetaMask/github-tools/pull/286))

### Fixed

- Count changed lines in `pr-line-check` from the pull request files API, so the count always reflects the pull request's current base branch ([#273](https://github.com/MetaMask/github-tools/pull/273))
Expand Down
Loading