Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/check_files_tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A workflow to test our tj-checkfiles limitations
name: Checkfiles Limitaitons
# Run on pull_request that is labeled as "optional_ci_tpu" or workflow dispatch
on:
pull_request:
paths:
- restricted_actions/changed_files
- .github/workflows/check_files_tests.yaml
branches:
- main
defaults:
run:
shell: bash
# Cancel any previous iterations if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
correct-permissions-test:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
# Verify correct permissions succeed
- name: Check Files
uses: ./restricted_actions/changed_files
52 changes: 52 additions & 0 deletions restricted_actions/changed_files/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2025 Google LLC

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# https://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Action to use tj-actions/changed-files in a locked permissions manner
# Usage of this composite is to ensure that tj-actions/changed-files can only be used
# with repository permissions read.
name: "Changed Files"
description: 'Changed files runs tj-actions/changed-files with forced limited permissions with the set of parameters allowed'
# The verified inputs we allow usage of from the composite
inputs:
files:
description: |
File and directory patterns used to detect changes (Defaults to the entire repo if unset).
NOTE: Multiline file/directory patterns should not include quotes.
required: false
default: ""
# Outputs with value to our usage
outputs:
all_changed_files:
description: "Returns all changed files i.e. a combination of all added, copied, modified and renamed files (ACMR)"
all_changed_files_count:
description: "Returns the number of `all_changed_files`"
any_changed:
description: "Returns `true` when any of the filenames provided using the `files*` or `files_ignore*` inputs have changed. This defaults to `true` when no patterns are specified. i.e. *includes a combination of all added, copied, modified and renamed files (ACMR)*."
only_changed:
description: "Returns `true` when only files provided using the `files*` or `files_ignore*` inputs have changed. i.e. *includes a combination of all added, copied, modified and renamed files (ACMR)*."
runs:
using: "composite"
steps:
# We force the permission but we also force best practice from our caller.
# We only permit this to run with contents: read and from a PR
- name: Verify Restrictions
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
$GITHUB_ACTION_PATH/check_permissions.sh

# - name: Get changed files
# id: changed-files
# uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46
107 changes: 107 additions & 0 deletions restricted_actions/changed_files/check_permissions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/bin/bash

# Script to check if a GitHub token has only 'contents:read' permission
# based on the X-OAuth-Scopes header returned by the GitHub API.

# This script aims to provide an answer based on the explicit scopes reported
# in the 'X-OAuth-Scopes' HTTP header.

# Expected scope we are checking for
EXPECTED_SCOPE="contents:read"

# Make a request to a simple GitHub API endpoint.
# Using /zen as it's lightweight and should return auth-related headers
# Capture stderr to check curl command output for errors, but don't print it directly unless necessary
API_RESPONSE_HEADERS=$(curl -s -I -H "Authorization: token $GITHUB_TOKEN" https://api.github.com 2>&1)
CURL_EXIT_CODE=$?

echo "$API_RESPONSE_HEADERS"

# Check if curl command itself failed (e.g., network issue, DNS resolution)
if [ $CURL_EXIT_CODE -ne 0 ]; then
echo "Error: curl command failed. Could not connect to GitHub API. (curl exit code: $CURL_EXIT_CODE)" >&2
# Avoid printing API_RESPONSE_HEADERS here as it might contain sensitive curl error details or the token.
exit 2 # Connection/curl error
fi

# Extract HTTP status code from the response headers
HTTP_STATUS_LINE=$(echo "$API_RESPONSE_HEADERS" | grep -i "^HTTP/")
HTTP_STATUS=$(echo "$HTTP_STATUS_LINE" | awk '{print $2}')

# Validate HTTP status
if [ -z "$HTTP_STATUS" ]; then
echo "Error: Could not retrieve HTTP status from GitHub API response." >&2
exit 3 # API communication error
fi

# If token is invalid or expired, GitHub returns 401 Unauthorized (shouldn't happen in an action)
if [ "$HTTP_STATUS" == "401" ]; then
echo "Error: GitHub API returned status $HTTP_STATUS. Token is likely invalid, expired, or revoked." >&2
exit 4 # Authentication error
fi

# Other non-200 statuses for /zen might indicate other issues (e.g. GitHub rate limiting, server errors)
# Though /zen itself is generally robust.
if [ "$HTTP_STATUS" != "200" ]; then
echo "Error: GitHub API returned non-200 status $HTTP_STATUS for /zen endpoint." >&2
# echo "Debug: API Response Headers:" >&2
# echo "$API_RESPONSE_HEADERS" >&2
exit 5 # Unexpected API status
fi

# Extract the X-OAuth-Scopes header (case-insensitive search for the header name)
OAUTH_SCOPES_LINE=$(echo "$API_RESPONSE_HEADERS" | grep -i "^X-OAuth-Scopes:")

# Check if the X-OAuth-Scopes header was found
if [ -z "$OAUTH_SCOPES_LINE" ]; then
# This can happen if the token is not a type that uses OAuth scopes,
# or if the token has no scopes assigned in a way that populates this header.
if echo "$API_RESPONSE_HEADERS" | grep -qi "WWW-Authenticate:"; then
echo "Error: Token authentication may have failed or was not processed as an OAuth token by GitHub." >&2
echo "The X-OAuth-Scopes header was missing, and a WWW-Authenticate header was present." >&2
else
echo "Error: No X-OAuth-Scopes header found in the API response." >&2
echo "The token might be of an unsupported type for this check, or it genuinely has no OAuth scopes reported." >&2
fi
exit 6 # Missing scopes header
fi

# Extract the value part of the scopes header.
# sed: remove "X-OAuth-Scopes: " (case-insensitive) and leading spaces from value.
# tr: remove potential carriage returns.
# xargs: trim leading/trailing whitespace from the final string.
ACTUAL_SCOPES_STRING=$(echo "$OAUTH_SCOPES_LINE" | sed -E 's/^[Xx]-[Oo][Aa]uth-[Ss]copes:[[:space:]]*//i' | tr -d '\r' | xargs)

# No permissions is a valid case
if [ -z "$ACTUAL_SCOPES_STRING" ] || [ "$ACTUAL_SCOPES_STRING" == "(no scope)" ]; then
echo "Token does not have any permissions"
exit 0
fi

# Parse the potentially comma-separated scopes into an array.
# Each scope in the array will be trimmed of leading/trailing whitespace.
IFS=',' read -r -a SCOPES_ARRAY <<< "$ACTUAL_SCOPES_STRING"
declare -a CLEANED_SCOPES
NUM_EFFECTIVE_SCOPES=0

for scope_item in "${SCOPES_ARRAY[@]}"; do
# Trim whitespace from individual scope_item using xargs
trimmed_item=$(echo "$scope_item" | xargs)
if [ -n "$trimmed_item" ]; then # Ensure it's not an empty string after trim (e.g. due to "scope1, , scope2")
CLEANED_SCOPES+=("$trimmed_item")
((NUM_EFFECTIVE_SCOPES++))
fi
done

# Now, check if the token has *exactly one* scope, and if that scope is the expected one.
if [ "$NUM_EFFECTIVE_SCOPES" -eq 1 ] && [ "${CLEANED_SCOPES[0]}" == "$EXPECTED_SCOPE" ]; then
echo "Token has only '$EXPECTED_SCOPE' permission."
exit 0 # Success
else
echo "Token does not have only '$EXPECTED_SCOPE' permission." >&2
echo "Detected scopes: $ACTUAL_SCOPES_STRING" >&2
# Optional: Print parsed scopes for debugging
echo "Parsed effective scopes ($NUM_EFFECTIVE_SCOPES):" >&2
for s_debug in "${CLEANED_SCOPES[@]}"; do echo "- '$s_debug'" >&2; done
exit 1 # Negative result (does not have the permission)
fi
Loading