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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Advanced usage with some custom configuration:
command: .restart # can be anything you want (example)
reaction: "eyes"
allowed_contexts: "pull_request,issue"
permissions: "write,admin"
permissions: "maintain,admin"
allowlist: monalisa
```

Expand Down Expand Up @@ -190,7 +190,7 @@ As seen above, we have a single example step. Perhaps you would actually use a r
| `success_reaction` | `true` | `+1` | The reaction to add to the comment that triggered the Action if its execution was successful |
| `failure_reaction` | `true` | `-1` | The reaction to add to the comment that triggered the Action if its execution failed |
| `allowed_contexts` | `true` | `pull_request` | A comma separated list of comment contexts that are allowed to trigger this IssueOps command. Pull requests and issues are the only currently supported contexts. To allow IssueOps commands to be invoked from both PRs and issues, set this option to the following: `"pull_request,issue"`. By default, the only place this Action will allow IssueOps commands from is pull requests |
| `permissions` | `true` | `"write,admin"` | The allowed GitHub permissions an actor can have to invoke IssueOps commands |
| `permissions` | `true` | `"write,admin"` | A comma separated list of allowed GitHub full role names or legacy base permissions |
| `allow_drafts` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on draft pull requests |
| `allow_forks` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on forked pull requests |
| `skip_ci` | `true` | `"false"` | Whether or not to require passing CI checks before this IssueOps command can be run |
Expand All @@ -201,6 +201,13 @@ As seen above, we have a single example step. Perhaps you would actually use a r
| `skip_completing` | `true` | `"false"` | If set to `"true"`, skip the process of completing the Action. This is useful if you want to customize the way this Action completes - For example, custom reactions, comments, etc |
| `fork_review_bypass` | `true` | `"false"` | If set to "true", allow forks to bypass the review requirement if the operation is being made on a pull request from a fork. This option is potentially dangerous if you are checking out code in your workflow as a result of invoking this Action. If the code you are checking out has not been reviewed, then you might open yourself up to a TOCTOU vulnerability. You should always ensure that the code you are checking out has been reviewed, and that you checkout an exact commit sha rather than a ref. |

The `permissions` input matches both the actor's full repository role name
(including `maintain`, `triage`, and custom roles) and GitHub's legacy base
permission. GitHub maps `maintain` to `write` and `triage` to `read` in the
legacy field, so the default `write,admin` continues to allow maintainers.
Use `maintain,admin` to allow maintainers and administrators while excluding
ordinary writers. Custom role names must match the value returned by GitHub.

## Outputs 📤

| Output | Description |
Expand Down
108 changes: 108 additions & 0 deletions __tests__/functions/valid-permissions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import * as core from '@actions/core'
import {validPermissions} from '../../src/functions/valid-permissions'

const setOutputMock = jest.spyOn(core, 'setOutput')
const mockPermissionResponse = (permission, roleName) =>
jest.fn().mockReturnValue({
status: 200,
data: {
permission,
...(roleName === undefined ? {} : {role_name: roleName})
}
})

var octokit
var context
Expand Down Expand Up @@ -49,6 +57,106 @@ test('determines that a user has does not valid permissions to invoke the Action
expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa')
})

test('allows a maintainer when maintain is configured', async () => {
process.env.INPUT_PERMISSIONS = 'maintain,admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'maintain'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('rejects an ordinary writer when only maintain and admin are configured', async () => {
process.env.INPUT_PERMISSIONS = 'maintain,admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'write'
)

expect(await validPermissions(octokit, context)).toEqual(
'👋 __monalisa__, seems as if you have not maintain/admin permissions in this repo, permissions: write'
)
})

test('keeps maintainers authorized by the default write and admin permissions', async () => {
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'maintain'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('allows an explicitly configured custom role', async () => {
process.env.INPUT_PERMISSIONS = 'release-manager,admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'release-manager'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('falls back to the base permission when role_name is missing', async () => {
process.env.INPUT_PERMISSIONS = 'read,admin'
octokit.rest.repos.getCollaboratorPermissionLevel =
mockPermissionResponse('read')

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('reports both full and base permissions when they differ', async () => {
process.env.INPUT_PERMISSIONS = 'admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'maintain'
)

const permissionError = await validPermissions(octokit, context)
expect(permissionError).toContain('permissions: maintain')
expect(permissionError).toContain('base permission: write')
})

test('allows a triage role when triage and admin are configured', async () => {
process.env.INPUT_PERMISSIONS = 'triage,admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'read',
'triage'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('rejects an ordinary reader when only triage and admin are configured', async () => {
process.env.INPUT_PERMISSIONS = 'triage,admin'
octokit.rest.repos.getCollaboratorPermissionLevel =
mockPermissionResponse('read')

expect(await validPermissions(octokit, context)).toEqual(
'👋 __monalisa__, seems as if you have not triage/admin permissions in this repo, permissions: read'
)
})

test('allows a triage role through configured base read compatibility', async () => {
process.env.INPUT_PERMISSIONS = 'read,admin'
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'read',
'triage'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('allows a custom role through the default write base permission', async () => {
octokit.rest.repos.getCollaboratorPermissionLevel = mockPermissionResponse(
'write',
'release-manager'
)

expect(await validPermissions(octokit, context)).toEqual(true)
})

test('fails to get actor permissions due to a bad status code', async () => {
octokit.rest.repos.getCollaboratorPermissionLevel = jest
.fn()
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ inputs:
required: true
default: "pull_request"
permissions:
description: 'The allowed GitHub permissions an actor can have to invoke IssueOps commands - Example: "write,admin"'
description: 'A comma separated list of allowed GitHub full role names or legacy base permissions - Example: "maintain,admin"'
required: true
default: "write,admin"
allow_drafts:
Expand Down
14 changes: 11 additions & 3 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions src/functions/valid-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ export async function validPermissions(octokit, context) {
return `Permission check returns non-200 status: ${permissionRes.status}`
}

// Check to ensure the user has at least write permission on the repo
// Check to ensure the user has a configured role or base permission on the repo
const actorPermission = permissionRes.data.permission
if (!validPermissionsArray.includes(actorPermission)) {
const actorRole = permissionRes.data.role_name
const hasValidPermission =
validPermissionsArray.includes(actorPermission) ||
(actorRole && validPermissionsArray.includes(actorRole))
if (!hasValidPermission) {
const displayedPermission =
actorRole && actorRole !== actorPermission
? `${actorRole} (base permission: ${actorPermission})`
: actorPermission
return `👋 __${
context.actor
}__, seems as if you have not ${validPermissionsArray.join(
'/'
)} permissions in this repo, permissions: ${actorPermission}`
)} permissions in this repo, permissions: ${displayedPermission}`
}

// Return true if the user has permissions
Expand Down