From 7229b783b7a26cf9f6be13b70b97aeddaca06314 Mon Sep 17 00:00:00 2001 From: HARPER Jon Date: Thu, 6 Mar 2025 15:13:51 +0100 Subject: [PATCH] Enforce permissions for releases and patches TODO: do this for all workflows without deduplicating this code The right way would be to create a script check_actor_permission.sh but this script is not checked out in the caller repo. There is no good workaround https://github.com/orgs/community/discussions/25289 https://github.com/orgs/community/discussions/25294 https://github.com/orgs/community/discussions/68735 https://github.com/orgs/community/discussions/63863 https://github.com/orgs/community/discussions/123261 The list goes on.. Signed-off-by: HARPER Jon --- .../workflows/patch-backend-app-generic.yml | 30 +++++++++++++++++++ .../workflows/patch-backend-lib-generic.yml | 30 +++++++++++++++++++ .../patch-base-docker-image-generic.yml | 30 +++++++++++++++++++ .../workflows/patch-frontend-app-generic.yml | 30 +++++++++++++++++++ .../workflows/release-backend-app-generic.yml | 30 +++++++++++++++++++ .../workflows/release-backend-lib-generic.yml | 30 +++++++++++++++++++ .../release-base-docker-image-generic.yml | 30 +++++++++++++++++++ .../release-frontend-app-generic.yml | 30 +++++++++++++++++++ .../release-frontend-lib-generic.yml | 30 +++++++++++++++++++ 9 files changed, 270 insertions(+) diff --git a/.github/workflows/patch-backend-app-generic.yml b/.github/workflows/patch-backend-app-generic.yml index e5c9f35..1900f1b 100644 --- a/.github/workflows/patch-backend-app-generic.yml +++ b/.github/workflows/patch-backend-app-generic.yml @@ -23,6 +23,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -37,6 +41,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - uses: actions/create-github-app-token@v1 id: app-token name: Generate app token diff --git a/.github/workflows/patch-backend-lib-generic.yml b/.github/workflows/patch-backend-lib-generic.yml index ad99a82..0e40e6e 100644 --- a/.github/workflows/patch-backend-lib-generic.yml +++ b/.github/workflows/patch-backend-lib-generic.yml @@ -12,6 +12,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -22,6 +26,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - name: Validate branch name format run: | if [[ ! "${{ inputs.branchRef }}" =~ ^release-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then diff --git a/.github/workflows/patch-base-docker-image-generic.yml b/.github/workflows/patch-base-docker-image-generic.yml index 8903274..466a02c 100644 --- a/.github/workflows/patch-base-docker-image-generic.yml +++ b/.github/workflows/patch-base-docker-image-generic.yml @@ -17,6 +17,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -29,6 +33,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - uses: actions/create-github-app-token@v1 id: app-token name: Generate app token diff --git a/.github/workflows/patch-frontend-app-generic.yml b/.github/workflows/patch-frontend-app-generic.yml index 38ce914..b6692de 100644 --- a/.github/workflows/patch-frontend-app-generic.yml +++ b/.github/workflows/patch-frontend-app-generic.yml @@ -20,6 +20,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -37,6 +41,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - name: Generate GitHub App Token id: app-token uses: actions/create-github-app-token@v1 diff --git a/.github/workflows/release-backend-app-generic.yml b/.github/workflows/release-backend-app-generic.yml index da67ade..d41c499 100644 --- a/.github/workflows/release-backend-app-generic.yml +++ b/.github/workflows/release-backend-app-generic.yml @@ -26,6 +26,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -40,6 +44,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - uses: actions/create-github-app-token@v1 id: app-token name: Generate app token diff --git a/.github/workflows/release-backend-lib-generic.yml b/.github/workflows/release-backend-lib-generic.yml index 81b4f03..a23003e 100644 --- a/.github/workflows/release-backend-lib-generic.yml +++ b/.github/workflows/release-backend-lib-generic.yml @@ -12,6 +12,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -22,6 +26,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - name: Validate Version Type run: | if [[ ! "${{ inputs.versionType }}" =~ ^(major|minor)$ ]]; then diff --git a/.github/workflows/release-base-docker-image-generic.yml b/.github/workflows/release-base-docker-image-generic.yml index 63cb243..aef8ad7 100644 --- a/.github/workflows/release-base-docker-image-generic.yml +++ b/.github/workflows/release-base-docker-image-generic.yml @@ -20,6 +20,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -32,6 +36,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - uses: actions/create-github-app-token@v1 id: app-token name: Generate app token diff --git a/.github/workflows/release-frontend-app-generic.yml b/.github/workflows/release-frontend-app-generic.yml index 3761c28..8e46e28 100644 --- a/.github/workflows/release-frontend-app-generic.yml +++ b/.github/workflows/release-frontend-app-generic.yml @@ -24,6 +24,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -41,6 +45,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - name: Generate GitHub App Token id: app-token uses: actions/create-github-app-token@v1 diff --git a/.github/workflows/release-frontend-lib-generic.yml b/.github/workflows/release-frontend-lib-generic.yml index 5ea5b14..c13061d 100644 --- a/.github/workflows/release-frontend-lib-generic.yml +++ b/.github/workflows/release-frontend-lib-generic.yml @@ -17,6 +17,10 @@ on: required: false default: 'release' type: string + permission: + required: false + default: 'maintain' # "pull" or "triage" or "push" or "maintain" or "admin" + type: string secrets: VERSIONBUMP_GHAPP_PRIVATE_KEY: required: true @@ -30,6 +34,32 @@ jobs: environment: name: ${{ inputs.environment }} steps: + # don't allow to disable this check, and for security only succeed if everything goes perfectly and outputs "true" + # TODO deduplicate this code when github actions allows to easily share a script used + # by reusable workflows. Currently only the workflow file is checked out at the caller + # There are no good workarounds to checkout a companion file (script or composite action) + - name: Check Actor Permission + env: + # Need to properly pass permission all the way down to jq as data to avoid script injections + PERMISSION: ${{ inputs.permission }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "checking permission=$PERMISSION on repo=$GITHUB_REPOSITORY for user=$GITHUB_ACTOR" + # no need to escape GITHUB_REPOSITORY and GITHUB_ACTOR because github + # names can only contain ASCII letters, digits, and the characters ., # -, and _. + # permission: legacy, is actually a role (one per user) and doesn't + # have "maintain" role, only ["none", "read", "write", "admin"] + # role_name: one per user, new standard roles, custom roles, "" and + # "push" and "pull" instead of "none" and "read" and "write": "", + # "read", "triage", "write", "maintain", "admin" and also non + # standard custom roles + # user.permissions: boolean status only for each standard permissions "pull", "triage", "push", "maintain", "admin" + # So the best for now is to use user.permissions I think. + # Not using gh --jq gojq because we can't use variables with it and also it automatically removes quotes from strings which could be an attack vector + # the gh api returns a JSON boolean so jq can only output true or false without quotes. + allowed=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission" | jq --arg PERMISSION "$PERMISSION" '.user.permissions[$PERMISSION]') + [[ "$allowed" == "true" ]] || exit 1 + - name: Validate Version Type run: | if [[ ! "${{ inputs.versionType }}" =~ ^(major|minor|patch)$ ]]; then