openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr4

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/checkActorPermissions.ts

135lines · 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
28/**
29 * Checks that the GitHub actor which triggered the current workflow has write
30 * access to the repository.
31 */
32export async function ensureActorHasWriteAccess(
33 options: EnsureWriteAccessOptions = {},
34): Promise<WriteAccessCheck> {
35 const actor = options.actor ?? process.env.GITHUB_ACTOR;
36 const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
37 const allowBotActors = options.allowBotActors ?? true;
38
39 if (!actor || actor.trim().length === 0) {
40 return {
41 status: "rejected",
42 actor: actor ?? "<unknown>",
43 reason: "GITHUB_ACTOR is not set; cannot determine triggering user.",
44 };
45 }
46
47 if (!repository || repository.trim().length === 0) {
48 return {
49 status: "rejected",
50 actor,
51 reason: "GITHUB_REPOSITORY is not set; cannot determine target repository.",
52 };
53 }
54
55 const [owner, repo] = repository.split("/");
56 if (!owner || !repo) {
57 return {
58 status: "rejected",
59 actor,
60 reason: `GITHUB_REPOSITORY must be in the format 'owner/repo', received: '${repository}'.`,
61 };
62 }
63
64 // GitHub-built workflows (e.g. dependabot, github-actions[bot]) do not have a
65 // meaningful write permission concept. They implicitly run with the token's permissions.
66 if (allowBotActors && actor.endsWith("[bot]")) {
67 core.info(`Actor '${actor}' is a bot account; skipping explicit permission check.`);
68 return { status: "approved", actor };
69 }
70
71 const token = options.token ?? getTokenFromEnv();
72 if (!token) {
73 return {
74 status: "rejected",
75 actor,
76 reason: "A GitHub token is required to check permissions (set GITHUB_TOKEN or GH_TOKEN).",
77 };
78 }
79
80 const octokit = options.octokit ?? new Octokit({ auth: token });
81
82 core.info(`Checking write access for actor '${actor}' on ${owner}/${repo}`);
83
84 let permission: string;
85 try {
86 const response = await octokit.repos.getCollaboratorPermissionLevel({
87 owner,
88 repo,
89 username: actor,
90 });
91 permission = response.data.permission ?? "none";
92 } catch (error) {
93 if (isNotFoundError(error)) {
94 return {
95 status: "rejected",
96 actor,
97 reason: `Actor '${actor}' is not a collaborator on ${owner}/${repo}; write access is required.`,
98 };
99 }
100
101 const message =
102 error instanceof Error
103 ? error.message
104 : "Failed to verify permissions for actor due to unknown error.";
105
106 return {
107 status: "rejected",
108 actor,
109 reason: `Failed to verify permissions for '${actor}': ${message}`,
110 };
111 }
112
113 core.info(`Actor '${actor}' has permission level '${permission}'.`);
114
115 if (permission === "admin" || permission === "write" || permission === "maintain") {
116 return { status: "approved", actor };
117 }
118
119 return {
120 status: "rejected",
121 actor,
122 reason: `Actor '${actor}' must have write access to ${owner}/${repo}. Detected permission: '${permission}'.`,
123 };
124}
125
126function getTokenFromEnv(): string {
127 const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
128 return token && token.trim().length > 0 ? token : "";
129}
130
131function isNotFoundError(error: unknown): boolean {
132 return Boolean(
133 error && typeof error === "object" && "status" in error && (error as { status?: number }).status === 404,
134 );
135}
136