Skip to content

Commit 6bac163

Browse files
committed
feat: validate GitHub App permissions via API and add tests
- Use `octokit.rest.users.getByUsername` to determine actor type (`User` or `Bot`) - Fetch GitHub App installation details with `octokit.rest.apps.getRepoInstallation` for permission validation. - Ensure GitHub Apps have `"issues"` permission set to `"write"` before allowing execution. - Add test cases for GitHub App actors
1 parent c6920ac commit 6bac163

File tree

4 files changed

+120
-3
lines changed

4 files changed

+120
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ As seen above, we have a single example step. Perhaps you would actually use a r
190190
| `success_reaction` | `true` | `+1` | The reaction to add to the comment that triggered the Action if its execution was successful |
191191
| `failure_reaction` | `true` | `-1` | The reaction to add to the comment that triggered the Action if its execution failed |
192192
| `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 |
193-
| `permissions` | `true` | `"write,admin"` | The allowed GitHub permissions an actor can have to invoke IssueOps commands |
193+
| `permissions` | `true` | `"write,admin"` | The allowed GitHub permissions an actor can have to invoke IssueOps commands. Note that permission check for GitHub App ignores this and instead expects "issues" permission set to "write". |
194194
| `allow_drafts` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on draft pull requests |
195195
| `allow_forks` | `true` | `"false"` | Whether or not to allow this IssueOps command to be run on forked pull requests |
196196
| `skip_ci` | `true` | `"false"` | Whether or not to require passing CI checks before this IssueOps command can be run |

__tests__/functions/valid-permissions.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,24 @@ beforeEach(() => {
1616

1717
octokit = {
1818
rest: {
19+
users: {
20+
getByUsername: jest.fn().mockReturnValueOnce({
21+
status: 200,
22+
data: {
23+
type: 'User'
24+
}
25+
})
26+
},
1927
repos: {
2028
getCollaboratorPermissionLevel: jest.fn().mockReturnValueOnce({
2129
status: 200,
2230
data: {
2331
permission: 'write'
2432
}
2533
})
34+
},
35+
apps: {
36+
getRepoInstallation: jest.fn()
2637
}
2738
}
2839
}
@@ -61,3 +72,71 @@ test('fails to get actor permissions due to a bad status code', async () => {
6172
)
6273
expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa')
6374
})
75+
76+
test('determines that a GitHub App has valid permissions', async () => {
77+
context.actor = 'github-actions[bot]'
78+
79+
octokit.rest.users.getByUsername.mockReturnValueOnce({
80+
status: 200,
81+
data: {
82+
type: 'Bot'
83+
}
84+
})
85+
86+
octokit.rest.apps.getRepoInstallation.mockReturnValueOnce({
87+
status: 200,
88+
data: {
89+
permissions: {
90+
issues: 'write'
91+
}
92+
}
93+
})
94+
95+
expect(await validPermissions(octokit, context)).toEqual(true)
96+
expect(setOutputMock).toHaveBeenCalledWith('actor', 'github-actions[bot]')
97+
})
98+
99+
test('determines that a GitHub App does not have valid permissions', async () => {
100+
context.actor = 'monalisa[bot]'
101+
102+
octokit.rest.users.getByUsername.mockReturnValueOnce({
103+
status: 200,
104+
data: {
105+
type: 'Bot'
106+
}
107+
})
108+
109+
octokit.rest.apps.getRepoInstallation.mockReturnValueOnce({
110+
status: 200,
111+
data: {
112+
permissions: {
113+
issues: 'read'
114+
}
115+
}
116+
})
117+
118+
expect(await validPermissions(octokit, context)).toEqual(
119+
'👋 __monalisa[bot]__ does not have "issues" permission set to "write". Current permissions: {"issues":"read"}'
120+
)
121+
expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa[bot]')
122+
})
123+
124+
test('fails to fetch installation details for GitHub App', async () => {
125+
context.actor = 'monalisa[bot]'
126+
127+
octokit.rest.users.getByUsername.mockReturnValueOnce({
128+
status: 200,
129+
data: {
130+
type: 'Bot'
131+
}
132+
})
133+
134+
octokit.rest.apps.getRepoInstallation.mockReturnValueOnce({
135+
status: 500
136+
})
137+
138+
expect(await validPermissions(octokit, context)).toEqual(
139+
'Failed to fetch GitHub App installation details: Status 500'
140+
)
141+
expect(setOutputMock).toHaveBeenCalledWith('actor', 'monalisa[bot]')
142+
})

action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ inputs:
3333
required: true
3434
default: "pull_request"
3535
permissions:
36-
description: 'The allowed GitHub permissions an actor can have to invoke IssueOps commands - Example: "write,admin"'
36+
description: 'The allowed GitHub permissions an actor can have to invoke IssueOps commands - Example: "write,admin". Permission check for GitHub App ignores this and instead expects "issues" permission set to "write".'
3737
required: true
3838
default: "write,admin"
3939
allow_drafts:
@@ -57,7 +57,7 @@ inputs:
5757
required: true
5858
default: "|"
5959
allowlist:
60-
description: 'A comma separated list of GitHub usernames or teams that should be allowed to use the IssueOps commands configured in this Action. If unset, then all users meeting the "permissions" requirement will be able to run commands. Example: "monalisa,octocat,my-org/my-team"'
60+
description: 'A comma separated list of GitHub usernames or teams that should be allowed to use the IssueOps commands configured in this Action. If unset, then all users meeting the "permissions" requirement will be able to run commands. Example: "monalisa,monalisa[bot],octocat,my-org/my-team"'
6161
required: false
6262
default: "false"
6363
allowlist_pat:

src/functions/valid-permissions.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,44 @@ export async function validPermissions(octokit, context) {
1313

1414
core.setOutput('actor', context.actor)
1515

16+
// Get Actor Type from GitHub API
17+
const userRes = await octokit.rest.users.getByUsername(
18+
{
19+
username: context.actor
20+
}
21+
)
22+
23+
if (userRes.status !== 200) {
24+
return `Fetch user details returns non-200 status: ${userRes.status}`
25+
}
26+
27+
const actorType = userRes.data.type; // "User" or "Bot"
28+
core.info(`🔍 Detected actor type: ${actorType} (${context.actor})`);
29+
30+
// Handle GitHub Apps (Bots)
31+
if (actorType === 'Bot') {
32+
// Fetch installation details for the GitHub App
33+
const installationRes = await octokit.rest.apps.getRepoInstallation(
34+
{
35+
owner: context.repo.owner,
36+
repo: context.repo.repo
37+
}
38+
)
39+
40+
if (installationRes.status !== 200) {
41+
return `Failed to fetch GitHub App installation details: Status ${installationRes.status}`
42+
}
43+
44+
const appPermissions = installationRes.data.permissions || {};
45+
46+
// Ensure the bot has "issues" permission set to "write"
47+
if (appPermissions.issues !== 'write') {
48+
return `👋 __${context.actor}__ does not have "issues" permission set to "write". Current permissions: ${JSON.stringify(appPermissions)}`;
49+
}
50+
51+
return true
52+
}
53+
1654
// Get the permissions of the user who made the comment
1755
const permissionRes = await octokit.rest.repos.getCollaboratorPermissionLevel(
1856
{

0 commit comments

Comments
 (0)