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 · modeblame

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