Resolve actor account type before applying allowed_bots (#1330)

Move the allowed_bots check in checkHumanActor and checkWritePermissions so
it only fires after the actor has been resolved as a non-User account
(GitHub App / bot, or unresolvable app actor). Actors that resolve to a
regular User account go through the standard human/write checks regardless
of allowed_bots.

The Copilot-style path (GITHUB_ACTOR not ending in [bot] and not resolvable
as a user) is unchanged: it still falls through to the existing 404 catch,
which already consults allowed_bots once the API has reported the actor is
not a user.

Update tests to match and add coverage for the User-account path.
This commit is contained in:
Ashwin Bhat
2026-05-19 16:30:49 -07:00
committed by GitHub
parent ca89df3d42
commit 1dc994ee7a
4 changed files with 147 additions and 47 deletions
+26 -22
View File
@@ -32,35 +32,32 @@ export async function checkHumanActor(
githubContext: GitHubContext,
) {
const allowedBots = githubContext.inputs.allowedBots;
const actor = githubContext.actor;
// Check allowed_bots BEFORE calling the GitHub Users API.
// Some bot actors (e.g. GitHub Copilot with GITHUB_ACTOR="Copilot") are
// not resolvable via the Users API and would cause a 404 if we called it
// first. By checking the allow-list early we avoid the unnecessary API
// call and the resulting crash.
if (isAllowedBot(githubContext.actor, allowedBots)) {
console.log(
`Actor ${githubContext.actor} is in allowed_bots list, skipping human actor check`,
);
return;
}
// Fetch user information from GitHub API
// Resolve the actor's account type before consulting allowed_bots so the
// allow-list only ever applies to non-User accounts. Some app actors
// (e.g. GitHub Copilot with GITHUB_ACTOR="Copilot") are not resolvable
// via the Users API and 404 — that path is handled in the catch below.
let actorType: string;
try {
const { data: userData } = await octokit.users.getByUsername({
username: githubContext.actor,
username: actor,
});
actorType = userData.type;
} catch (error) {
// Handle 404 for non-user actors (GitHub Apps whose GITHUB_ACTOR
// doesn't match any user account, e.g. "Copilot").
if (
error instanceof Error &&
(error.message.includes("Not Found") ||
error.message.includes("is not a user"))
) {
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
// Unresolvable actors are GitHub Apps without a backing user account.
if (isAllowedBot(actor, allowedBots)) {
console.log(
`Actor ${actor} is in allowed_bots list, skipping human actor check`,
);
return;
}
const botName = actor.toLowerCase().replace(/\[bot\]$/, "");
throw new Error(
`Workflow initiated by non-human actor: ${botName} (actor not found on GitHub). Add bot to allowed_bots list or use '*' to allow all bots.`,
);
@@ -70,15 +67,22 @@ export async function checkHumanActor(
console.log(`Actor type: ${actorType}`);
// Check bot permissions if actor is not a User
if (actorType !== "User") {
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
// Bot not allowed (we already checked allowed_bots above)
// GitHub Apps and other bot accounts.
if (isAllowedBot(actor, allowedBots)) {
console.log(
`Actor ${actor} is in allowed_bots list, skipping human actor check`,
);
return;
}
const botName = actor.toLowerCase().replace(/\[bot\]$/, "");
throw new Error(
`Workflow initiated by non-human actor: ${botName} (type: ${actorType}). Add bot to allowed_bots list or use '*' to allow all bots.`,
);
}
console.log(`Verified human actor: ${githubContext.actor}`);
// Regular User account. allowed_bots is only for bot actors and is not
// consulted here; write-access enforcement for users happens separately
// in checkWritePermissions.
console.log(`Verified human actor: ${actor}`);
}
+8 -11
View File
@@ -66,22 +66,19 @@ export async function checkWritePermissions(
}
}
// Check if the actor is a GitHub App (bot user with [bot] suffix)
// Check if the actor is a GitHub App (bot user with [bot] suffix).
// Usernames cannot contain "[" or "]", so the suffix is a reliable
// bot signal that doesn't require an API lookup.
if (actor.endsWith("[bot]")) {
core.info(`Actor is a GitHub App: ${actor}`);
return true;
}
// Check if the actor is in the allowed bots list (handles non-[bot] actors
// like GitHub Copilot whose GITHUB_ACTOR is "Copilot", not "Copilot[bot]")
if (isAllowedBot(actor, allowedBots)) {
core.info(
`Actor ${actor} is in allowed_bots list, skipping permission check`,
);
return true;
}
// Check permissions directly using the permission endpoint
// For all other actors, resolve the account via the collaborator
// permission endpoint. allowed_bots is only consulted in the catch
// block below, after the API has confirmed the actor is not a regular
// user account (e.g. GitHub Apps like Copilot whose GITHUB_ACTOR is
// "Copilot" rather than "Copilot[bot]").
const response = await octokit.repos.getCollaboratorPermissionLevel({
owner: repository.owner,
repo: repository.repo,