diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 398c95d4..468fa108 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -11,13 +11,104 @@ on: types: [submitted] jobs: - claude: + authorize_claude_actor: if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest + permissions: + contents: read + outputs: + authorized: ${{ steps.permission-gate.outputs.authorized }} + steps: + - name: Verify actor can run Claude + id: permission-gate + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const permissionByLogin = new Map(); + + function claudeRequester() { + if ( + context.eventName === 'issue_comment' || + context.eventName === 'pull_request_review_comment' + ) { + return context.payload.comment?.user?.login; + } + + if (context.eventName === 'pull_request_review') { + return context.payload.review?.user?.login; + } + + if (context.eventName === 'issues') { + return context.payload.issue?.user?.login; + } + + return null; + } + + async function hasWriteAccessFor(username) { + if (!username) { + return false; + } + + if (permissionByLogin.has(username)) { + return permissionByLogin.get(username); + } + + let hasWriteAccess = false; + try { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username + }); + hasWriteAccess = ['admin', 'write'].includes(permission.permission); + } catch (error) { + core.warning( + `Unable to verify ${username} write permission for Claude; ` + + `treating the Claude request as untrusted: ${error.message || error}` + ); + } + + permissionByLogin.set(username, hasWriteAccess); + return hasWriteAccess; + } + + const actor = context.actor; + const requester = claudeRequester(); + + if (!requester) { + core.setOutput('authorized', 'false'); + core.setFailed('Unable to run Claude because the requester could not be determined.'); + return; + } + + if (!(await hasWriteAccessFor(actor))) { + core.setOutput('authorized', 'false'); + core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`); + return; + } + + if (!(await hasWriteAccessFor(requester))) { + core.setOutput('authorized', 'false'); + core.setFailed( + `Unable to run Claude because requester ${requester} does not have write/admin repository permission.` + ); + return; + } + + core.setOutput('authorized', 'true'); + core.info(`Authorized ${actor} and Claude requester ${requester} to run Claude.`); + + claude: + needs: authorize_claude_actor + if: needs.authorize_claude_actor.outputs.authorized == 'true' + runs-on: ubuntu-latest permissions: contents: read pull-requests: read