From 374c1885f14d05141c7154f6a2a159f50e478890 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Sep 2025 22:50:25 +0000 Subject: [PATCH] chore: bump Claude Code version to 1.0.108 --- .github/workflows/issue-triage.yml | 1 + action.yml | 5 ++ docs/security.md | 5 ++ docs/usage.md | 49 +++++------ src/entrypoints/prepare.ts | 4 + src/github/context.ts | 2 + src/github/validation/permissions.ts | 26 ++++++ test/install-mcp-server.test.ts | 1 + test/mockContext.ts | 1 + test/permissions.test.ts | 123 +++++++++++++++++++++++++++ 10 files changed, 193 insertions(+), 24 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index fe092a6..b4d3d4d 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -102,6 +102,7 @@ jobs: prompt: $(cat /tmp/claude-prompts/triage-prompt.txt) anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} + allowed_non_write_users: "*" claude_args: | --allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues --mcp-config /tmp/mcp-config/mcp-servers.json diff --git a/action.yml b/action.yml index 8e44688..f8ece92 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,10 @@ inputs: description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots." required: false default: "" + allowed_non_write_users: + description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)." + required: false + default: "" # Claude Code configuration prompt: @@ -148,6 +152,7 @@ runs: BRANCH_PREFIX: ${{ inputs.branch_prefix }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }} ALLOWED_BOTS: ${{ inputs.allowed_bots }} + ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }} GITHUB_RUN_ID: ${{ github.run_id }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} diff --git a/docs/security.md b/docs/security.md index 45ea4f2..e23429b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,6 +4,11 @@ - **Repository Access**: The action can only be triggered by users with write access to the repository - **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots +- **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature: + - Only works when `github_token` is provided as input (not with GitHub App authentication) + - Accepts either a comma-separated list of specific usernames or `*` to allow all users + - **Should be used with extreme caution** as it bypasses the primary security mechanism of this action + - Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope - **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in - **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered - **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions diff --git a/docs/usage.md b/docs/usage.md index 58cc1fa..9ceadd7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -47,30 +47,31 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | -| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | -| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | -| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | -| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | -| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | -| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | -| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | -| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | -| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | -| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | -| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | -| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | -| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | -| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | -| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | -| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | +| Input | Description | Required | Default | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- | +| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | +| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | +| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | +| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | +| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | +| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | +| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | +| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | +| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | +| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | +| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | +| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | +| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | +| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | +| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | +| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | +| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | ### Deprecated Inputs diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 84a31bc..af0ce9d 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -30,9 +30,13 @@ async function run() { // Step 3: Check write permissions (only for entity contexts) if (isEntityContext(context)) { + // Check if github_token was provided as input (not from app) + const githubTokenProvided = !!process.env.OVERRIDE_GITHUB_TOKEN; const hasWritePermissions = await checkWritePermissions( octokit.rest, context, + context.inputs.allowedNonWriteUsers, + githubTokenProvided, ); if (!hasWritePermissions) { throw new Error( diff --git a/src/github/context.ts b/src/github/context.ts index de4dd08..56a9233 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -93,6 +93,7 @@ type BaseContext = { botId: string; botName: string; allowedBots: string; + allowedNonWriteUsers: string; trackProgress: boolean; }; }; @@ -147,6 +148,7 @@ export function parseGitHubContext(): GitHubContext { botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, allowedBots: process.env.ALLOWED_BOTS ?? "", + allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "", trackProgress: process.env.TRACK_PROGRESS === "true", }, }; diff --git a/src/github/validation/permissions.ts b/src/github/validation/permissions.ts index e571e3a..731fcd4 100644 --- a/src/github/validation/permissions.ts +++ b/src/github/validation/permissions.ts @@ -6,17 +6,43 @@ import type { Octokit } from "@octokit/rest"; * Check if the actor has write permissions to the repository * @param octokit - The Octokit REST client * @param context - The GitHub context + * @param allowedNonWriteUsers - Comma-separated list of users allowed without write permissions, or '*' for all + * @param githubTokenProvided - Whether github_token was provided as input (not from app) * @returns true if the actor has write permissions, false otherwise */ export async function checkWritePermissions( octokit: Octokit, context: ParsedGitHubContext, + allowedNonWriteUsers?: string, + githubTokenProvided?: boolean, ): Promise { const { repository, actor } = context; try { core.info(`Checking permissions for actor: ${actor}`); + // Check if we should bypass permission checks for this user + if (allowedNonWriteUsers && githubTokenProvided) { + const allowedUsers = allowedNonWriteUsers.trim(); + if (allowedUsers === "*") { + core.warning( + `⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.`, + ); + return true; + } else if (allowedUsers) { + const allowedUserList = allowedUsers + .split(",") + .map((u) => u.trim()) + .filter((u) => u.length > 0); + if (allowedUserList.includes(actor)) { + core.warning( + `⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.`, + ); + return true; + } + } + } + // Check if the actor is a GitHub App (bot user) if (actor.endsWith("[bot]")) { core.info(`Actor is a GitHub App: ${actor}`); diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 48b54be..41879d6 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -35,6 +35,7 @@ describe("prepareMcpConfig", () => { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }, }; diff --git a/test/mockContext.ts b/test/mockContext.ts index c375f18..73255e6 100644 --- a/test/mockContext.ts +++ b/test/mockContext.ts @@ -23,6 +23,7 @@ const defaultInputs = { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }; diff --git a/test/permissions.test.ts b/test/permissions.test.ts index 6659d62..9aeb301 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -71,6 +71,7 @@ describe("checkWritePermissions", () => { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }, }); @@ -175,4 +176,126 @@ describe("checkWritePermissions", () => { username: "test-user", }); }); + + describe("allowed_non_write_users bypass", () => { + test("should bypass permission check for specific user when github_token provided", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + "test-user,other-user", + true, + ); + + expect(result).toBe(true); + expect(coreWarningSpy).toHaveBeenCalledWith( + "⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.", + ); + }); + + test("should bypass permission check for all users with wildcard", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + "*", + true, + ); + + expect(result).toBe(true); + expect(coreWarningSpy).toHaveBeenCalledWith( + "⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.", + ); + }); + + test("should NOT bypass permission check when user not in allowed list", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + "other-user,another-user", + true, + ); + + expect(result).toBe(false); + expect(coreWarningSpy).toHaveBeenCalledWith( + "Actor has insufficient permissions: read", + ); + }); + + test("should NOT bypass permission check when github_token not provided", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + "test-user", + false, + ); + + expect(result).toBe(false); + expect(coreWarningSpy).toHaveBeenCalledWith( + "Actor has insufficient permissions: read", + ); + }); + + test("should NOT bypass permission check when allowed_non_write_users is empty", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + "", + true, + ); + + expect(result).toBe(false); + expect(coreWarningSpy).toHaveBeenCalledWith( + "Actor has insufficient permissions: read", + ); + }); + + test("should handle whitespace in allowed_non_write_users list", async () => { + const mockOctokit = createMockOctokit("read"); + const context = createContext(); + + const result = await checkWritePermissions( + mockOctokit, + context, + " test-user , other-user ", + true, + ); + + expect(result).toBe(true); + expect(coreWarningSpy).toHaveBeenCalledWith( + "⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.", + ); + }); + + test("should bypass for bot users even when allowed_non_write_users is set", async () => { + const mockOctokit = createMockOctokit("none"); + const context = createContext(); + context.actor = "test-bot[bot]"; + + const result = await checkWritePermissions( + mockOctokit, + context, + "some-user", + true, + ); + + expect(result).toBe(true); + expect(coreInfoSpy).toHaveBeenCalledWith( + "Actor is a GitHub App: test-bot[bot]", + ); + }); + }); });