openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr59

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/checkActorPermissions.ts

167lines · 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 (default), bot actors such as dependabot are allowed without
22 * checking collaborator permissions. Set to false to require bots to pass the
23 * same checks as human 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
33/**
34 * Checks that the GitHub actor which triggered the current workflow has write
35 * access to the repository.
36 */
37export async function ensureActorHasWriteAccess(
38 options: EnsureWriteAccessOptions = {},
39): Promise<WriteAccessCheck> {
40 const actor = options.actor ?? process.env.GITHUB_ACTOR;
41 const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
42 const allowBotActors = options.allowBotActors ?? true;
43
44 if (!actor || actor.trim().length === 0) {
45 return {
46 status: "rejected",
47 actor: actor ?? "<unknown>",
48 reason: "GITHUB_ACTOR is not set; cannot determine triggering user.",
49 };
50 }
51
52 if (!repository || repository.trim().length === 0) {
53 return {
54 status: "rejected",
55 actor,
56 reason: "GITHUB_REPOSITORY is not set; cannot determine target repository.",
57 };
58 }
59
60 const [owner, repo] = repository.split("/");
61 if (!owner || !repo) {
62 return {
63 status: "rejected",
64 actor,
65 reason: `GITHUB_REPOSITORY must be in the format 'owner/repo', received: '${repository}'.`,
66 };
67 }
68
69 // GitHub-built workflows (e.g. dependabot, github-actions[bot]) do not have a
70 // meaningful write permission concept. They implicitly run with the token's permissions.
71 if (allowBotActors && actor.endsWith("[bot]")) {
72 core.info(`Actor '${actor}' is a bot account; skipping explicit permission check.`);
73 return { status: "approved", actor };
74 }
75
76 // Allow-list override: if allowUsers is '*' allow all users. If it is a
77 // comma-separated list, allow listed users (case-insensitive) without checking
78 // collaborator permissions.
79 const allowUsersSpec = (options.allowUsers ?? "").trim();
80 if (allowUsersSpec.length > 0) {
81 if (allowUsersSpec === "*") {
82 core.info("allow-users='*' specified; allowing all users to proceed.");
83 return { status: "approved", actor };
84 }
85 const allowed = new Set(
86 allowUsersSpec
87 .split(",")
88 .map((s) => s.trim().toLowerCase())
89 .filter((s) => s.length > 0),
90 );
91 if (allowed.has(actor.toLowerCase())) {
92 core.info(`Actor '${actor}' is explicitly allowed via allow-users.`);
93 return { status: "approved", actor };
94 }
95 }
96
97 const token = options.token ?? getTokenFromEnv();
98 if (!token) {
99 return {
100 status: "rejected",
101 actor,
102 reason: "A GitHub token is required to check permissions (set GITHUB_TOKEN or GH_TOKEN).",
103 };
104 }
105
106 const baseUrl = process.env.GITHUB_API_URL?.trim();
107 const octokit =
108 options.octokit ??
109 new Octokit({
110 auth: token,
111 ...(baseUrl ? { baseUrl } : {}),
112 });
113
114 core.info(`Checking write access for actor '${actor}' on ${owner}/${repo}`);
115
116 let permission: string;
117 try {
118 const response = await octokit.repos.getCollaboratorPermissionLevel({
119 owner,
120 repo,
121 username: actor,
122 });
123 permission = response.data.permission ?? "none";
124 } catch (error) {
125 if (isNotFoundError(error)) {
126 return {
127 status: "rejected",
128 actor,
129 reason: `Actor '${actor}' is not a collaborator on ${owner}/${repo}; write access is required.`,
130 };
131 }
132
133 const message =
134 error instanceof Error
135 ? error.message
136 : "Failed to verify permissions for actor due to unknown error.";
137
138 return {
139 status: "rejected",
140 actor,
141 reason: `Failed to verify permissions for '${actor}': ${message}`,
142 };
143 }
144
145 core.info(`Actor '${actor}' has permission level '${permission}'.`);
146
147 if (permission === "admin" || permission === "write" || permission === "maintain") {
148 return { status: "approved", actor };
149 }
150
151 return {
152 status: "rejected",
153 actor,
154 reason: `Actor '${actor}' must have write access to ${owner}/${repo}. Detected permission: '${permission}'.`,
155 };
156}
157
158function getTokenFromEnv(): string {
159 const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
160 return token && token.trim().length > 0 ? token : "";
161}
162
163function isNotFoundError(error: unknown): boolean {
164 return Boolean(
165 error && typeof error === "object" && "status" in error && (error as { status?: number }).status === 404,
166 );
167}