openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/checkActorPermissions.ts

161lines · 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 octokit = options.octokit ?? new Octokit({ auth: token });
107
108 core.info(`Checking write access for actor '${actor}' on ${owner}/${repo}`);
109
110 let permission: string;
111 try {
112 const response = await octokit.repos.getCollaboratorPermissionLevel({
113 owner,
114 repo,
115 username: actor,
116 });
117 permission = response.data.permission ?? "none";
118 } catch (error) {
119 if (isNotFoundError(error)) {
120 return {
121 status: "rejected",
122 actor,
123 reason: `Actor '${actor}' is not a collaborator on ${owner}/${repo}; write access is required.`,
124 };
125 }
126
127 const message =
128 error instanceof Error
129 ? error.message
130 : "Failed to verify permissions for actor due to unknown error.";
131
132 return {
133 status: "rejected",
134 actor,
135 reason: `Failed to verify permissions for '${actor}': ${message}`,
136 };
137 }
138
139 core.info(`Actor '${actor}' has permission level '${permission}'.`);
140
141 if (permission === "admin" || permission === "write" || permission === "maintain") {
142 return { status: "approved", actor };
143 }
144
145 return {
146 status: "rejected",
147 actor,
148 reason: `Actor '${actor}' must have write access to ${owner}/${repo}. Detected permission: '${permission}'.`,
149 };
150}
151
152function getTokenFromEnv(): string {
153 const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
154 return token && token.trim().length > 0 ? token : "";
155}
156
157function isNotFoundError(error: unknown): boolean {
158 return Boolean(
159 error && typeof error === "object" && "status" in error && (error as { status?: number }).status === 404,
160 );
161}
162