openai/codex-action

Public

mirrored from https://github.com/openai/codex-actionAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/checkActorPermissions.ts

215lines · modecode

1import * as core from "@actions/core";
2import { Octokit } from "@octokit/rest";
3
4export type WriteAccessCheck =
5 | {
6 status: "approved";
7 actor: string;
8 }
9 | {
10 status: "rejected";
11 actor: string;
12 reason: string;
13 };
14
15type EnsureWriteAccessOptions = {
16 octokit?: Octokit;
17 token?: string;
18 actor?: string;
19 repository?: string;
20 /**
21 * When true, trusted GitHub bot actors are allowed without checking
22 * collaborator permissions. Other bots must pass the same checks as human
23 * users.
24 */
25 allowBotActors?: boolean;
26 /**
27 * Comma-separated list of allowed GitHub usernames or '*' to allow all users.
28 * Case-insensitive; empty string or undefined disables this override.
29 */
30 allowUsers?: string;
31 /**
32 * Comma-separated list of allowed GitHub bot usernames. '*' is not supported.
33 * Entries may include or omit the trailing [bot] suffix.
34 */
35 allowBotUsers?: string;
36};
37
38/**
39 * Checks that the GitHub actor which triggered the current workflow has write
40 * access to the repository.
41 */
42export async function ensureActorHasWriteAccess(
43 options: EnsureWriteAccessOptions = {},
44): Promise<WriteAccessCheck> {
45 const actor = options.actor ?? process.env.GITHUB_ACTOR;
46 const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
47 const allowBotActors = options.allowBotActors ?? false;
48
49 if (!actor || actor.trim().length === 0) {
50 return {
51 status: "rejected",
52 actor: actor ?? "<unknown>",
53 reason: "GITHUB_ACTOR is not set; cannot determine triggering user.",
54 };
55 }
56
57 if (!repository || repository.trim().length === 0) {
58 return {
59 status: "rejected",
60 actor,
61 reason: "GITHUB_REPOSITORY is not set; cannot determine target repository.",
62 };
63 }
64
65 const [owner, repo] = repository.split("/");
66 if (!owner || !repo) {
67 return {
68 status: "rejected",
69 actor,
70 reason: `GITHUB_REPOSITORY must be in the format 'owner/repo', received: '${repository}'.`,
71 };
72 }
73
74 // GitHub-built workflows do not have a meaningful collaborator permission
75 // level. Only trust the built-in bot actors GitHub owns.
76 if (allowBotActors && isTrustedGitHubBotActor(actor)) {
77 core.info(`Actor '${actor}' is a trusted GitHub bot account; skipping explicit permission check.`);
78 return { status: "approved", actor };
79 }
80
81 const allowedBotActors = parseAllowedBotActors(options.allowBotUsers ?? "");
82 if (allowedBotActors instanceof Error) {
83 return {
84 status: "rejected",
85 actor,
86 reason: allowedBotActors.message,
87 };
88 }
89 if (isBotActor(actor) && allowedBotActors.has(normalizeBotActor(actor))) {
90 core.info(`Actor '${actor}' is explicitly allowed via allow-bot-users.`);
91 return { status: "approved", actor };
92 }
93
94 // Allow-list override: if allowUsers is '*' allow all users. If it is a
95 // comma-separated list, allow listed users (case-insensitive) without checking
96 // collaborator permissions.
97 const allowUsersSpec = (options.allowUsers ?? "").trim();
98 if (allowUsersSpec.length > 0) {
99 if (allowUsersSpec === "*") {
100 core.info("allow-users='*' specified; allowing all users to proceed.");
101 return { status: "approved", actor };
102 }
103 const allowed = new Set(
104 allowUsersSpec
105 .split(",")
106 .map((s) => s.trim().toLowerCase())
107 .filter((s) => s.length > 0),
108 );
109 if (allowed.has(actor.toLowerCase())) {
110 core.info(`Actor '${actor}' is explicitly allowed via allow-users.`);
111 return { status: "approved", actor };
112 }
113 }
114
115 const token = options.token ?? getTokenFromEnv();
116 if (!token) {
117 return {
118 status: "rejected",
119 actor,
120 reason: "A GitHub token is required to check permissions (set GITHUB_TOKEN or GH_TOKEN).",
121 };
122 }
123
124 const baseUrl = process.env.GITHUB_API_URL?.trim();
125 const octokit =
126 options.octokit ??
127 new Octokit({
128 auth: token,
129 ...(baseUrl ? { baseUrl } : {}),
130 });
131
132 core.info(`Checking write access for actor '${actor}' on ${owner}/${repo}`);
133
134 let permission: string;
135 try {
136 const response = await octokit.repos.getCollaboratorPermissionLevel({
137 owner,
138 repo,
139 username: actor,
140 });
141 permission = response.data.permission ?? "none";
142 } catch (error) {
143 if (isNotFoundError(error)) {
144 return {
145 status: "rejected",
146 actor,
147 reason: `Actor '${actor}' is not a collaborator on ${owner}/${repo}; write access is required.`,
148 };
149 }
150
151 const message =
152 error instanceof Error
153 ? error.message
154 : "Failed to verify permissions for actor due to unknown error.";
155
156 return {
157 status: "rejected",
158 actor,
159 reason: `Failed to verify permissions for '${actor}': ${message}`,
160 };
161 }
162
163 core.info(`Actor '${actor}' has permission level '${permission}'.`);
164
165 if (permission === "admin" || permission === "write" || permission === "maintain") {
166 return { status: "approved", actor };
167 }
168
169 return {
170 status: "rejected",
171 actor,
172 reason: `Actor '${actor}' must have write access to ${owner}/${repo}. Detected permission: '${permission}'.`,
173 };
174}
175
176function getTokenFromEnv(): string {
177 const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
178 return token && token.trim().length > 0 ? token : "";
179}
180
181const TRUSTED_GITHUB_BOT_ACTORS = new Set(["github-actions[bot]"]);
182
183function isTrustedGitHubBotActor(actor: string): boolean {
184 return isBotActor(actor) && TRUSTED_GITHUB_BOT_ACTORS.has(normalizeBotActor(actor));
185}
186
187function parseAllowedBotActors(allowBotUsers: string): Set<string> | Error {
188 const allowed = new Set<string>();
189 for (const entry of allowBotUsers.split(",")) {
190 const bot = entry.trim();
191 if (bot.length === 0) {
192 continue;
193 }
194 if (bot.includes("*")) {
195 return new Error("allow-bot-users does not support '*'; list trusted bot usernames explicitly.");
196 }
197 allowed.add(normalizeBotActor(bot));
198 }
199 return allowed;
200}
201
202function isBotActor(actor: string): boolean {
203 return actor.toLowerCase().endsWith("[bot]");
204}
205
206function normalizeBotActor(actor: string): string {
207 const normalized = actor.toLowerCase();
208 return isBotActor(normalized) ? normalized : `${normalized}[bot]`;
209}
210
211function isNotFoundError(error: unknown): boolean {
212 return Boolean(
213 error && typeof error === "object" && "status" in error && (error as { status?: number }).status === 404,
214 );
215}
216