From 7fbea466c902e674feba3941ee1166391bc8e3ff Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 26 Aug 2025 10:30:37 -0700 Subject: [PATCH] feat: Add comprehensive security controls to showtime-trigger workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add maintainer authorization check to prevent unauthorized workflow execution - Validate SHA input format to prevent injection attacks - Add 90-minute timeout protection against runaway jobs - Implement automatic blocking for PR synchronize events when Showtime is active - Add unlabeled trigger support for proper label removal handling - Preserve local customizations (install-docker-compose: false, upgrade flag) Security improvements protect against arbitrary code execution while maintaining workflow_dispatch convenience for authorized maintainers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/showtime-trigger.yml | 74 ++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/.github/workflows/showtime-trigger.yml b/.github/workflows/showtime-trigger.yml index 7915177bbc8..aaae8b0d177 100644 --- a/.github/workflows/showtime-trigger.yml +++ b/.github/workflows/showtime-trigger.yml @@ -3,7 +3,7 @@ name: đŸŽĒ Superset Showtime # Ultra-simple: just sync on any PR state change on: pull_request_target: - types: [labeled, synchronize, closed] + types: [labeled, unlabeled, synchronize, closed] # Manual testing workflow_dispatch: @@ -16,6 +16,7 @@ on: description: 'Specific SHA to deploy (optional, defaults to latest)' required: false type: string + pattern: '^[a-f0-9]{40}$' # Common environment variables for all jobs env: @@ -33,18 +34,83 @@ jobs: sync: name: đŸŽĒ Sync PR to desired state runs-on: ubuntu-latest + timeout-minutes: 90 permissions: contents: read pull-requests: write steps: + - name: Security Check - Authorize Maintainers Only + id: auth + uses: actions/github-script@v7 + with: + script: | + const actor = context.actor; + console.log(`🔍 Checking authorization for ${actor}`); + + // Early exit for workflow_dispatch - assume authorized since it's manually triggered + if (context.eventName === 'workflow_dispatch') { + console.log(`✅ Workflow dispatch event - assuming authorized for ${actor}`); + core.setOutput('authorized', 'true'); + return; + } + + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: actor + }); + + console.log(`📊 Permission level for ${actor}: ${permission.permission}`); + const authorized = ['write', 'admin'].includes(permission.permission); + + if (!authorized) { + console.log(`🚨 Unauthorized user ${actor} - skipping all operations`); + core.setOutput('authorized', 'false'); + return; + } + + console.log(`✅ Authorized maintainer: ${actor}`); + core.setOutput('authorized', 'true'); + + // If this is a synchronize event, check if Showtime is active and set blocked label + if (context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') { + console.log(`🔒 Synchronize event detected - checking if Showtime is active`); + + // Check if PR has any circus tent labels (Showtime is in use) + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number + }); + + const hasCircusLabels = issue.labels.some(label => label.name.startsWith('đŸŽĒ ')); + + if (hasCircusLabels) { + console.log(`đŸŽĒ Circus labels found - setting blocked label to prevent auto-deployment`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['đŸŽĒ 🔒 showtime-blocked'] + }); + + console.log(`✅ Blocked label set - Showtime will detect and skip operations`); + } else { + console.log(`â„šī¸ No circus labels found - Showtime not in use, skipping block`); + } + } + - name: Install Superset Showtime + if: steps.auth.outputs.authorized == 'true' run: | pip install --upgrade superset-showtime showtime version - name: Check what actions are needed + if: steps.auth.outputs.authorized == 'true' id: check run: | # Bulletproof PR number extraction @@ -79,14 +145,14 @@ jobs: echo "target_sha=$TARGET_SHA" >> $GITHUB_OUTPUT - name: Checkout PR code (only if build needed) - if: steps.check.outputs.build_needed == 'true' + if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true' uses: actions/checkout@v4 with: ref: ${{ steps.check.outputs.target_sha }} persist-credentials: false - name: Setup Docker Environment (only if build needed) - if: steps.check.outputs.build_needed == 'true' + if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true' uses: ./.github/actions/setup-docker with: dockerhub-user: ${{ env.DOCKERHUB_USER }} @@ -95,7 +161,7 @@ jobs: install-docker-compose: "false" - name: Execute sync (handles everything) - if: steps.check.outputs.sync_needed == 'true' + if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.sync_needed == 'true' run: | PR_NUM="${{ steps.check.outputs.pr_number }}" TARGET_SHA="${{ steps.check.outputs.target_sha }}"