Browse Source

Create action to validate permissions of the GitHub actor that triggered a workflow run (#493)

pull/494/head
Mick Letofsky 2 weeks ago committed by GitHub
parent
commit
01e6323e22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 67
      .github/workflows/_check-permission.yml
  2. 145
      .github/workflows/test-check-permission.yml
  3. 223
      check-permission/EXAMPLES.md
  4. 118
      check-permission/README.md
  5. 113
      check-permission/action.yml

67
.github/workflows/_check-permission.yml

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
name: Permission Check
on:
workflow_call:
inputs:
failure_mode:
description: "Permission check failure mode: fail, skip, or continue"
type: string
default: "fail"
required: false
require_permission:
description: "Required permission level: admin, write, read, or none"
type: string
default: "write"
required: false
outputs:
has_permission:
description: "Whether the user has the required permission"
value: ${{ jobs.check-permission.outputs.has_permission }}
user_permission:
description: "The actual permission level of the user"
value: ${{ jobs.check-permission.outputs.user_permission }}
should_skip:
description: "Whether subsequent jobs should be skipped"
value: ${{ jobs.check-permission.outputs.should_skip }}
permissions:
contents: read
jobs:
check-permission:
name: Check permission
runs-on: ubuntu-24.04
outputs:
has_permission: ${{ steps.check.outputs.has_permission }}
user_permission: ${{ steps.check.outputs.user_permission }}
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- name: Check out repo
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check user permission
id: check
uses: bitwarden/gh-actions/check-permission@main
with:
require: ${{ inputs.require_permission }}
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: ${{ inputs.failure_mode }}
- name: Report results
env:
ACTOR: ${{ github.triggering_actor }}
REQUIRED: ${{ inputs.require_permission }}
ACTUAL: ${{ steps.check.outputs.user_permission }}
HAS_PERM: ${{ steps.check.outputs.has_permission }}
SHOULD_SKIP: ${{ steps.check.outputs.should_skip }}
run: |
echo "🤖 Permission Check"
echo "================================"
echo "User: $ACTOR"
echo "Required: $REQUIRED"
echo "Actual: $ACTUAL"
echo "Has permission: $HAS_PERM"
echo "Should skip: $SHOULD_SKIP"

145
.github/workflows/test-check-permission.yml

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
name: Test Check Permission Action
on:
pull_request:
paths:
- "check-permission/**"
- ".github/workflows/test-check-permission.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
test-fail-mode:
name: Test Fail Mode
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check permission (should pass for repo collaborators)
id: check-fail
uses: ./check-permission
with:
require: read
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: fail
- name: Verify output
env:
HAS_PERMISSION: ${{ steps.check-fail.outputs.has_permission }}
USER_PERMISSION: ${{ steps.check-fail.outputs.user_permission }}
SHOULD_SKIP: ${{ steps.check-fail.outputs.should_skip }}
run: |
echo "Has permission: $HAS_PERMISSION"
echo "User permission: $USER_PERMISSION"
echo "Should skip: $SHOULD_SKIP"
test-skip-mode:
name: Test skip mode
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check permission (always succeeds with skip mode)
id: check-skip
uses: ./check-permission
with:
require: admin
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- name: Conditional step
if: steps.check-skip.outputs.should_skip != 'true'
run: echo "Would run privileged operation"
- name: Skip notification
if: steps.check-skip.outputs.should_skip == 'true'
run: echo "Skipped privileged operation due to permissions"
test-continue-mode:
name: Test continue mode
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check permission with continue mode
id: permission
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: continue
- name: Admin operations
if: steps.permission.outputs.user_permission == 'admin'
run: echo "Running admin-level operations"
- name: Write operations
if: steps.permission.outputs.user_permission == 'write'
run: echo "Running write-level operations"
- name: Read operations
if: steps.permission.outputs.user_permission == 'read'
run: echo "Running read-level operations"
- name: Always runs
env:
USER_PERMISSION: ${{ steps.permission.outputs.user_permission }}
HAS_PERMISSION: ${{ steps.permission.outputs.has_permission }}
run: |
echo "This step always runs"
echo "User permission: $USER_PERMISSION"
echo "Has required permission: $HAS_PERMISSION"
test-fallback-pattern:
name: Test fallback pattern
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Check admin permission (preferred)
id: check-admin
uses: ./check-permission
with:
require: admin
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- name: Check write permission (fallback)
if: steps.check-admin.outputs.should_skip == 'true'
id: check-write
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- name: Report results
env:
ACTOR: ${{ github.triggering_actor }}
ADMIN_PERM: ${{ steps.check-admin.outputs.has_permission }}
WRITE_PERM: ${{ steps.check-write.outputs.has_permission }}
ACTUAL_PERM: ${{ steps.check-admin.outputs.user_permission }}
run: |
echo "User: $ACTOR"
echo "Admin permission: $ADMIN_PERM"
echo "Write permission: $WRITE_PERM"
echo "Actual permission: $ACTUAL_PERM"

223
check-permission/EXAMPLES.md

@ -0,0 +1,223 @@ @@ -0,0 +1,223 @@
# Examples
## Basic Usage
### Fail Mode (default)
Workflow stops if permission missing.
```yaml
steps:
- uses: bitwarden/gh-actions/check-permission@main
with:
require: admin
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
```
### Skip Mode
Workflow continues, skip protected steps.
```yaml
steps:
- id: check
uses: bitwarden/gh-actions/check-permission@main
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- if: steps.check.outputs.should_skip != 'true'
run: ./deploy.sh
```
### Continue Mode
Branch on actual permission level.
```yaml
steps:
- id: check
uses: bitwarden/gh-actions/check-permission@main
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: continue
- if: steps.check.outputs.user_permission == 'admin'
run: ./admin-deploy.sh
- if: steps.check.outputs.user_permission == 'write'
run: ./standard-deploy.sh
- if: steps.check.outputs.user_permission == 'read'
run: ./read-only.sh
```
## Advanced Patterns
### Multi-Environment Deploy
```yaml
jobs:
check:
outputs:
permission: ${{ steps.check.outputs.user_permission }}
steps:
- uses: actions/checkout@v4
- id: check
uses: ./check-permission
with:
require: read
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: continue
prod:
needs: check
if: needs.check.outputs.permission == 'admin'
steps:
- run: ./deploy-prod.sh
staging:
needs: check
if: needs.check.outputs.permission == 'write'
steps:
- run: ./deploy-staging.sh
dev:
needs: check
if: needs.check.outputs.permission == 'read'
steps:
- run: ./deploy-dev.sh
```
### Permission Gate
```yaml
jobs:
gate:
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- uses: actions/checkout@v4
- id: check
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
deploy:
needs: gate
if: needs.gate.outputs.should_skip != 'true'
steps:
- run: ./deploy.sh
```
### Fallback Strategy
```yaml
steps:
- id: admin
uses: ./check-permission
with:
require: admin
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- if: steps.admin.outputs.should_skip == 'true'
id: write
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: fail
- if: steps.admin.outputs.has_permission == 'true'
run: ./full-deploy.sh
- if: steps.admin.outputs.should_skip == 'true'
run: ./standard-deploy.sh
```
## Reusable Workflows
### Flexible Consumer Control
```yaml
# reusable-deploy.yml
on:
workflow_call:
inputs:
failure_mode:
type: string
default: 'fail'
jobs:
check:
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- uses: actions/checkout@v4
- id: check
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: ${{ inputs.failure_mode }}
deploy:
needs: check
if: needs.check.outputs.should_skip != 'true'
steps:
- run: ./deploy.sh
```
### Job-Level Outputs
```yaml
on:
workflow_call:
outputs:
deployed:
value: ${{ jobs.check.outputs.has_permission }}
jobs:
check:
outputs:
has_permission: ${{ steps.check.outputs.has_permission }}
steps:
- uses: actions/checkout@v4
- id: check
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
deploy:
needs: check
if: needs.check.outputs.has_permission == 'true'
steps:
- run: ./deploy.sh
```
## Best Practices
- Use `fail` for critical operations
- Use `skip` for optional features
- Use `continue` for tiered functionality
- Specify `failure_mode` explicitly
- Test with different permission levels
- Handle bot accounts separately
- Use outputs for conditional logic
- Document permission requirements

118
check-permission/README.md

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
# Check Permission Action
Check user permissions with configurable failure handling.
## Features
- Check permissions: admin, write, read, none
- Three modes: fail, skip, or continue
- Control workflow execution based on permission level
- Works in reusable workflows
## Inputs
| Input | Description | Required | Default |
| -------------- | ------------------------------------------------------------ | -------- | ------- |
| `require` | Required permission level (`admin`, `write`, `read`, `none`) | Yes | - |
| `username` | Username to check permissions for | Yes | - |
| `token` | GitHub token for API access | Yes | - |
| `failure_mode` | How to handle failures: `fail`, `skip`, or `continue` | No | `fail` |
### Failure Modes
- **`fail`**: Exit 1 when permission missing - workflow stops
- **`skip`**: Exit 0, set `should_skip=true` - skip protected steps
- **`continue`**: Exit 0 always - branch on `has_permission` output
## Outputs
| Output | Description |
| ----------------- | ---------------------------------------------------------------------- |
| `has_permission` | `true` if user has required permission, `false` otherwise |
| `user_permission` | Actual permission level of the user (`admin`, `write`, `read`, `none`) |
| `should_skip` | `true` when failure_mode is `skip` and permission check fails |
## Usage Examples
### Hard Fail (default)
```yaml
- uses: bitwarden/gh-actions/check-permission@main
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
```
### Skip Mode
```yaml
- id: permission
uses: bitwarden/gh-actions/check-permission@main
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: skip
- if: steps.permission.outputs.should_skip != 'true'
run: ./deploy.sh
```
### Continue Mode
```yaml
- id: permission
uses: bitwarden/gh-actions/check-permission@main
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: continue
- if: steps.permission.outputs.user_permission == 'admin'
run: ./admin-deploy.sh
- if: steps.permission.outputs.user_permission == 'write'
run: ./standard-deploy.sh
```
### Reusable Workflow
```yaml
on:
workflow_call:
inputs:
failure_mode:
type: string
default: 'fail'
jobs:
check:
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- uses: actions/checkout@v4
- id: check
uses: ./check-permission
with:
require: write
username: ${{ github.triggering_actor }}
token: ${{ secrets.GITHUB_TOKEN }}
failure_mode: ${{ inputs.failure_mode }}
deploy:
needs: check
if: needs.check.outputs.should_skip != 'true'
steps:
- run: ./deploy.sh
```
## Permissions
Requires `contents: read` permission. The default `GITHUB_TOKEN` works.
```yaml
permissions:
contents: read
```

113
check-permission/action.yml

@ -0,0 +1,113 @@ @@ -0,0 +1,113 @@
name: "Check Permission"
description: "A composite action that checks if a user has the required permission level with configurable failure modes"
author: "Bitwarden"
branding:
icon: shield
color: blue
inputs:
require:
description: "Required permission level (admin, write, read, none)"
required: true
username:
description: "Username to check permissions for"
required: true
token:
description: "GitHub token for API access"
required: true
failure_mode:
description: "How to handle permission failures: 'fail' (exit 1), 'skip' (exit 0 with skip output), 'continue' (exit 0 always, use outputs for branching)"
required: false
default: "fail"
outputs:
has_permission:
description: "Whether the user has the required permission (true/false)"
value: ${{ steps.check-permission.outputs.has_permission }}
user_permission:
description: "The actual permission level of the user"
value: ${{ steps.check-permission.outputs.user_permission }}
should_skip:
description: "Whether the workflow should skip (true when failure_mode is 'skip' and permission check fails)"
value: ${{ steps.check-permission.outputs.should_skip }}
runs:
using: "composite"
steps:
- name: Check user permission
id: check-permission
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
REQUIRE: ${{ inputs.require }}
USERNAME: ${{ inputs.username }}
FAILURE_MODE: ${{ inputs.failure_mode }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -e
if [[ ! "$FAILURE_MODE" =~ ^(fail|skip|continue)$ ]]; then
echo "::error::Invalid failure_mode; it must be 'fail', 'skip', or 'continue'"
exit 1
fi
if [[ ! "$REQUIRE" =~ ^(admin|write|read|none)$ ]]; then
echo "::error::Invalid require level; it must be 'admin', 'write', 'read', or 'none'"
exit 1
fi
if [[ -z "$USERNAME" ]] || [[ ! "$USERNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?(\[bot\])?$ ]]; then
echo "::error::Invalid username format. Username cannot be empty and must match GitHub's username requirements."
exit 1
fi
if ! USER_PERMISSION=$(gh api \
"repos/$GITHUB_REPOSITORY/collaborators/$USERNAME/permission" \
--jq '.permission // "none"' 2>&1); then
echo "::warning::Failed to check user permission: $USER_PERMISSION"
if [[ "$FAILURE_MODE" == "fail" ]]; then
echo "::error::Permission check failed and failure_mode is 'fail'"
exit 1
else
# Treat API failure as "no permission" for skip/continue modes
echo "::notice::Treating API failure as 'none' permission due to failure_mode='$FAILURE_MODE'"
USER_PERMISSION="none"
fi
fi
echo "user_permission=$USER_PERMISSION" >> "$GITHUB_OUTPUT"
declare -A PERM_LEVELS=( ["none"]=0 ["read"]=1 ["write"]=2 ["admin"]=3 )
USER_LEVEL=${PERM_LEVELS[$USER_PERMISSION]:-0}
REQUIRED_LEVEL=${PERM_LEVELS[$REQUIRE]}
# Check if user has required permission
if [ $USER_LEVEL -ge $REQUIRED_LEVEL ]; then
echo "✓ User has required permission"
echo "has_permission=true" >> "$GITHUB_OUTPUT"
echo "should_skip=false" >> "$GITHUB_OUTPUT"
exit 0
else
echo "✗ User does not have required permission"
echo "has_permission=false" >> "$GITHUB_OUTPUT"
case "$FAILURE_MODE" in
fail)
echo "::error::User '$USERNAME' does not have required '$REQUIRE' permission."
echo "should_skip=false" >> "$GITHUB_OUTPUT"
exit 1
;;
skip)
echo "::warning::User '$USERNAME' does not have required '$REQUIRE' permission. Marking for skip."
echo "should_skip=true" >> "$GITHUB_OUTPUT"
exit 0
;;
continue)
echo "::notice::User '$USERNAME' does not have required '$REQUIRE' permission. Continuing - check outputs for branching."
echo "should_skip=false" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
fi
Loading…
Cancel
Save