Check permissions in a GitHub Actions workflow

03 Apr 2023 in Tech

When working with GitHub Actions, you may want to check what relationship the person performing an action has to a repo before running a workflow. Public documentation on collaborators is scarce, so here’s what I’ve been able to work out so far.

  • There are two ways to check relationships: pull_request.author_association and the /repos/:org/:repo/collaborators/:user/permission endpoint.
  • pull_request.author_association returns an item from CommentAuthorAssociation, which is one of the following:
    1. COLLABORATOR: Author has been invited to collaborate on the repository.
    2. CONTRIBUTOR: Author has previously committed to the repository.
    3. FIRST_TIMER: Author has not previously committed to GitHub.
    4. FIRST_TIME_CONTRIBUTOR: Author has not previously committed to the repository.
    5. MANNEQUIN: Author is a placeholder for an unclaimed user.
    6. MEMBER: Author is a member of the organization that owns the repository.
    7. NONE: Author has no association with the repository.
    8. OWNER: Author is the owner of the repository.
  • The /repos/:org/:repo/collaborators/:user/permission endpoint returns the permissions that the provided user has on a repository. This is one of: read, write, admin

Author Association

The author association field is available in the default pull_request payload, which makes it useful if you don’t want to make an additional API call to figure out if an actor should be allowed to perform an action or not.

When interacting with an organization's repo, the pull_request.author_association value is different depending on if the actor is an external collaborator, an org member, or neither.

If the actor is neither an org member nor an external collaborator their author_association will be one of the following:

  • FIRST_TIMER: The actor has never contributed to any repository on GitHub before
  • FIRST_TIME_CONTRIBUTOR: The actor has never contributed to this repository before
  • CONTRIBUTOR: The actor has previously contributed to the repository

If the actor has been added as an external collaborator, their author_association will be COLLABORATOR, no matter which set of permissions they’ve been given. If they have read permissions, they’re a COLLABORATOR. If they have admin permissions, they’re still a COLLABORATOR.

Finally, if the actor is a member of the organization that owns the repository their author_association will be MEMBER. This is true whether they are an organization member or owner. In addition, their author_association will always be MEMBER regardless of the permissions they have on the repository. If they have read permissions, they’re a MEMBER. If they have admin permissions, they’re still a MEMBER.

The only other state that you might be interested in is OWNER. This state is not possible with organization owned repositories. It is only returned when a repository is owned by a specific user.

That leaves us with 2 unused states:

  • MANNEQUIN - Mannequins are created when source code and metadata are migrated from one source control platform to GitHub. If the user was not properly mapped to a GitHub account at the time of migration a placeholder mannequin is created. If you’re interested in seeing a mannequin, check out this issue.
  • NONE - I’m not sure how to trigger this as any actor is implicitly a FIRST_TIMER, FIRST_TIME_CONTRIBUTOR or CONTRIBUTOR

Finally, let’s talk about deleted users. Any deleted user actions are attributed to ghost, which is a GitHub owned account that “takes the place of user accounts that have been deleted 👻”

User Permissions

If you’re looking for more granularity than the data provided in author_association you can use the /repos/:org/:repo/collaborators/:user/permission endpoint to fetch which permission the actor has for this repo.

Note: You’ll need push permission on the repository to be able to use the endpoint above.

The possible values returned by this repository are as follows:

  • none - :repo is a private repository and the user has no permissions
  • read - The user has read or triage permissions, or :repo is a public repository and the user has no permissions
  • write - The user has write or maintain permissions
  • admin - The user has admin permission

The permission level provided is the same no matter how the permission is granted. The actor may be added as an external collaborator or to an organization team. As long as they have permission to the repository somehow, it will be returned via the endpoint above.

If you’d like to check a user’s permission in a workflow before performing a step, I recommend the Has Permission action.

Here’s an example from their README (with a small change to use github.token rather than a secret):

yaml
name: Action Sample Workflow
# Run workflow when a new pull request is opened
on: [pull_request]
jobs:
check_user_permission:
runs-on: ubuntu-latest
name: A job to check user's permission level
steps:
# Check for write permission
- name: Check user permission
id: check
uses: scherermichael-oss/action-has-permission@master
with:
required-permission: write
env:
GITHUB_TOKEN: $
# Use the output from the `check` step
- name: Run only if user has sufficient permissions
if: steps.check.outputs.has-permission
run: echo "Congratulations! Your permissions to access the repository are sufficient."
- name: Run only if user has NOT sufficient permissions
if: "! steps.check.outputs.has-permission"
run: echo "Sorry! Your permissions are insufficient."

When to use each option

So, which should we use? It depends what you want to check.

pull_request.author_association should be used when you want to check the relationship between the person performing an action and the repository itself. It’s useful when you want to auto-approve workflows for people that have contributed in the past (CONTRIBUTOR) or for everyone in your organization (MEMBER).

yaml
steps:
- name: Check access
if: ${{ github.event.pull_request.author_association != 'CONTRIBUTOR' && github.event.pull_request.author_association != 'MEMBER' }}
run: |
echo "Event not triggered by a collaborator."
exit 1

If you need to check for specific permissions, the /repos/:org/:repo/collaborators/:user/permission endpoint is the option to choose. This is useful when you work for an organization where only specific teams have write access to a repository and should be allowed to perform specific tasks.

If you’re not sure which to use, I’d recommend using the permission endpoint (via scherermichael-oss/action-has-permission) as it allows for more granular access controls than pull_request.author_association.