openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr19

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/checkActorPermissions.ts

161lines · modeblame

e9d0a695Michael Bolin10 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/**
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*/
25allowBotActors?: boolean;
b8d81868Michael Bolin10 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;
e9d0a695Michael Bolin10 months ago31};
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(
38options: EnsureWriteAccessOptions = {},
39): Promise<WriteAccessCheck> {
40const actor = options.actor ?? process.env.GITHUB_ACTOR;
41const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
42const allowBotActors = options.allowBotActors ?? true;
43
44if (!actor || actor.trim().length === 0) {
45return {
46status: "rejected",
47actor: actor ?? "<unknown>",
48reason: "GITHUB_ACTOR is not set; cannot determine triggering user.",
49};
50}
51
52if (!repository || repository.trim().length === 0) {
53return {
54status: "rejected",
55actor,
56reason: "GITHUB_REPOSITORY is not set; cannot determine target repository.",
57};
58}
59
60const [owner, repo] = repository.split("/");
61if (!owner || !repo) {
62return {
63status: "rejected",
64actor,
65reason: `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.
71if (allowBotActors && actor.endsWith("[bot]")) {
72core.info(`Actor '${actor}' is a bot account; skipping explicit permission check.`);
73return { status: "approved", actor };
74}
75
b8d81868Michael Bolin10 months ago76// 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.
79const allowUsersSpec = (options.allowUsers ?? "").trim();
80if (allowUsersSpec.length > 0) {
81if (allowUsersSpec === "*") {
82core.info("allow-users='*' specified; allowing all users to proceed.");
83return { status: "approved", actor };
84}
85const allowed = new Set(
86allowUsersSpec
87.split(",")
88.map((s) => s.trim().toLowerCase())
89.filter((s) => s.length > 0),
90);
91if (allowed.has(actor.toLowerCase())) {
92core.info(`Actor '${actor}' is explicitly allowed via allow-users.`);
93return { status: "approved", actor };
94}
95}
96
e9d0a695Michael Bolin10 months ago97const token = options.token ?? getTokenFromEnv();
98if (!token) {
99return {
100status: "rejected",
101actor,
102reason: "A GitHub token is required to check permissions (set GITHUB_TOKEN or GH_TOKEN).",
103};
104}
105
106const octokit = options.octokit ?? new Octokit({ auth: token });
107
108core.info(`Checking write access for actor '${actor}' on ${owner}/${repo}`);
109
110let permission: string;
111try {
112const response = await octokit.repos.getCollaboratorPermissionLevel({
113owner,
114repo,
115username: actor,
116});
117permission = response.data.permission ?? "none";
118} catch (error) {
119if (isNotFoundError(error)) {
120return {
121status: "rejected",
122actor,
123reason: `Actor '${actor}' is not a collaborator on ${owner}/${repo}; write access is required.`,
124};
125}
126
127const message =
128error instanceof Error
129? error.message
130: "Failed to verify permissions for actor due to unknown error.";
131
132return {
133status: "rejected",
134actor,
135reason: `Failed to verify permissions for '${actor}': ${message}`,
136};
137}
138
139core.info(`Actor '${actor}' has permission level '${permission}'.`);
140
141if (permission === "admin" || permission === "write" || permission === "maintain") {
142return { status: "approved", actor };
143}
144
145return {
146status: "rejected",
147actor,
148reason: `Actor '${actor}' must have write access to ${owner}/${repo}. Detected permission: '${permission}'.`,
149};
150}
151
152function getTokenFromEnv(): string {
153const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
154return token && token.trim().length > 0 ? token : "";
155}
156
157function isNotFoundError(error: unknown): boolean {
158return Boolean(
159error && typeof error === "object" && "status" in error && (error as { status?: number }).status === 404,
160);
161}