From 47dcd3f359eddccd387399ae3a2020c31cc7bbd7 Mon Sep 17 00:00:00 2001 From: Michael Hudgins Date: Wed, 7 May 2025 15:27:24 +0000 Subject: [PATCH 1/4] Start testing idea on restricted actions --- .github/workflows/check_files_tests.yaml | 26 +++++++++ restricted_actions/changed_files/action.yaml | 61 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/check_files_tests.yaml create mode 100644 restricted_actions/changed_files/action.yaml diff --git a/.github/workflows/check_files_tests.yaml b/.github/workflows/check_files_tests.yaml new file mode 100644 index 00000000..87ae6726 --- /dev/null +++ b/.github/workflows/check_files_tests.yaml @@ -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 diff --git a/restricted_actions/changed_files/action.yaml b/restricted_actions/changed_files/action.yaml new file mode 100644 index 00000000..2224a0e6 --- /dev/null +++ b/restricted_actions/changed_files/action.yaml @@ -0,0 +1,61 @@ +# 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 + run: | + echo "Verifying permissions of the github token are as restricted as expected" + # Use curl to get the token scopes from the GitHub API + scopes=$(curl -s -I -H "Authorization: token ${github.token}" https://api.github.com/ | grep "X-OAuth-Scopes" | awk '{print $2}') + + # Check if the request was successful + if [ -z "${scopes}" ]; then + echo "Error: Unable to retrieve token scopes. Please check your token and try again." + exit 1 + fi + # Print the token scopes + echo "GitHub Token Scopes:" + echo "${scopes}" + + # - name: Get changed files + # id: changed-files + # uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46 From 5c92e6991edffa17f6b9cd6786f1a2e7f77d1a1c Mon Sep 17 00:00:00 2001 From: Michael Hudgins Date: Wed, 7 May 2025 15:32:18 +0000 Subject: [PATCH 2/4] Change token --- restricted_actions/changed_files/action.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/restricted_actions/changed_files/action.yaml b/restricted_actions/changed_files/action.yaml index 2224a0e6..3121db58 100644 --- a/restricted_actions/changed_files/action.yaml +++ b/restricted_actions/changed_files/action.yaml @@ -42,14 +42,16 @@ runs: # We only permit this to run with contents: read and from a PR - name: Verify Restrictions shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} run: | echo "Verifying permissions of the github token are as restricted as expected" # Use curl to get the token scopes from the GitHub API - scopes=$(curl -s -I -H "Authorization: token ${github.token}" https://api.github.com/ | grep "X-OAuth-Scopes" | awk '{print $2}') + scopes=$(curl -s -I -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/ | grep "X-OAuth-Scopes" | awk '{print $2}') # Check if the request was successful if [ -z "${scopes}" ]; then - echo "Error: Unable to retrieve token scopes. Please check your token and try again." + echo "Error: Unable to retrieve token scopes." exit 1 fi # Print the token scopes From faee7b1acb1bf4d2010bb05f8a48831abcd224b9 Mon Sep 17 00:00:00 2001 From: Michael Hudgins Date: Wed, 7 May 2025 15:44:53 +0000 Subject: [PATCH 3/4] Add a permission limiter --- restricted_actions/changed_files/action.yaml | 13 +-- .../changed_files/check_permissions.sh | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 12 deletions(-) create mode 100755 restricted_actions/changed_files/check_permissions.sh diff --git a/restricted_actions/changed_files/action.yaml b/restricted_actions/changed_files/action.yaml index 3121db58..d9c30013 100644 --- a/restricted_actions/changed_files/action.yaml +++ b/restricted_actions/changed_files/action.yaml @@ -45,18 +45,7 @@ runs: env: GITHUB_TOKEN: ${{ github.token }} run: | - echo "Verifying permissions of the github token are as restricted as expected" - # Use curl to get the token scopes from the GitHub API - scopes=$(curl -s -I -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/ | grep "X-OAuth-Scopes" | awk '{print $2}') - - # Check if the request was successful - if [ -z "${scopes}" ]; then - echo "Error: Unable to retrieve token scopes." - exit 1 - fi - # Print the token scopes - echo "GitHub Token Scopes:" - echo "${scopes}" + $GITHUB_ACTION_PATH/check_permissions.sh # - name: Get changed files # id: changed-files diff --git a/restricted_actions/changed_files/check_permissions.sh b/restricted_actions/changed_files/check_permissions.sh new file mode 100755 index 00000000..a2706c80 --- /dev/null +++ b/restricted_actions/changed_files/check_permissions.sh @@ -0,0 +1,105 @@ +#!/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: Bearer $GITHUB_TOKEN" https://api.github.com/zen 2>&1) +CURL_EXIT_CODE=$? + +# 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 \ No newline at end of file From 3b14a98102833d55b2b5fd644f3a6bb3dc0350e7 Mon Sep 17 00:00:00 2001 From: Michael Hudgins Date: Wed, 7 May 2025 15:56:18 +0000 Subject: [PATCH 4/4] change token auth --- restricted_actions/changed_files/check_permissions.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/restricted_actions/changed_files/check_permissions.sh b/restricted_actions/changed_files/check_permissions.sh index a2706c80..f5ec933b 100755 --- a/restricted_actions/changed_files/check_permissions.sh +++ b/restricted_actions/changed_files/check_permissions.sh @@ -12,9 +12,11 @@ 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: Bearer $GITHUB_TOKEN" https://api.github.com/zen 2>&1) +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