From 283d5054ec5663795e8f9d744260c6c57b85f9e3 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 6 Jul 2026 18:02:54 +0200 Subject: [PATCH] ci: Set and enforce more default permissions (#60222) Release Notes: - N/A --- .github/workflows/after_release.yml | 2 + .github/workflows/autofix_pr.yml | 5 + .github/workflows/bump_collab_staging.yml | 5 + .github/workflows/bump_patch_version.yml | 4 + .github/workflows/bump_zed_version.yml | 9 ++ .github/workflows/cherry_pick.yml | 2 + .../comment_on_potential_duplicate_issues.yml | 2 + .../community_close_stale_issues.yml | 5 + ...ommunity_update_all_top_ranking_issues.yml | 6 + ...unity_update_weekly_top_ranking_issues.yml | 6 + .github/workflows/compliance_check.yml | 2 + .github/workflows/congrats.yml | 3 + .github/workflows/danger.yml | 2 + .github/workflows/deploy_collab.yml | 2 + .github/workflows/deploy_docs.yml | 2 + .github/workflows/deploy_nightly_docs.yml | 1 + .github/workflows/docs_suggestions.yml | 4 + .github/workflows/extension_auto_bump.yml | 2 + .github/workflows/extension_bump.yml | 2 + .github/workflows/extension_tests.yml | 2 + .../workflows/extension_workflow_rollout.yml | 2 + .../workflows/good_first_issue_notifier.yml | 3 + .github/workflows/hotfix-review-monitor.yml | 3 +- .github/workflows/nix_build.yml | 2 + .github/workflows/publish_extension_cli.yml | 2 + .github/workflows/release.yml | 4 + .github/workflows/release_nightly.yml | 2 + .github/workflows/run_bundling.yml | 2 + .github/workflows/run_tests.yml | 2 + ...ck_notify_community_automation_failure.yml | 3 + .../slack_notify_first_responders.yml | 3 + .../workflows/slack_notify_label_created.yml | 3 + .github/workflows/stale-pr-reminder.yml | 3 +- .../workflows/update_duplicate_magnets.yml | 6 + extensions/workflows/run_tests.yml | 2 + extensions/workflows/shared/bump_version.yml | 1 + tooling/xtask/src/tasks/workflow_checks.rs | 147 +++++++++++++----- .../workflow_checks/check_permissions.rs | 74 +++++++++ .../workflow_checks/check_run_patterns.rs | 71 ++------- tooling/xtask/src/tasks/workflows.rs | 4 +- .../src/tasks/workflows/after_release.rs | 5 +- .../xtask/src/tasks/workflows/autofix_pr.rs | 10 +- .../src/tasks/workflows/bump_patch_version.rs | 4 +- .../src/tasks/workflows/bump_zed_version.rs | 10 +- .../xtask/src/tasks/workflows/cherry_pick.rs | 6 +- .../src/tasks/workflows/compliance_check.rs | 3 +- tooling/xtask/src/tasks/workflows/danger.rs | 3 +- .../src/tasks/workflows/deploy_collab.rs | 4 +- .../xtask/src/tasks/workflows/deploy_docs.rs | 6 +- .../tasks/workflows/extension_auto_bump.rs | 3 +- .../src/tasks/workflows/extension_bump.rs | 8 +- .../src/tasks/workflows/extension_tests.rs | 3 +- .../workflows/extension_workflow_rollout.rs | 5 +- .../workflows/extensions/bump_version.rs | 1 + .../tasks/workflows/extensions/run_tests.rs | 1 + .../xtask/src/tasks/workflows/nix_build.rs | 3 +- .../tasks/workflows/publish_extension_cli.rs | 5 +- tooling/xtask/src/tasks/workflows/release.rs | 10 +- .../src/tasks/workflows/release_nightly.rs | 5 +- .../xtask/src/tasks/workflows/run_bundling.rs | 6 +- .../xtask/src/tasks/workflows/run_tests.rs | 5 +- tooling/xtask/src/tasks/workflows/steps.rs | 11 ++ 62 files changed, 388 insertions(+), 136 deletions(-) create mode 100644 tooling/xtask/src/tasks/workflow_checks/check_permissions.rs diff --git a/.github/workflows/after_release.yml b/.github/workflows/after_release.yml index 9fb93ee27d5..5a833447e9e 100644 --- a/.github/workflows/after_release.yml +++ b/.github/workflows/after_release.yml @@ -22,6 +22,8 @@ on: description: body type: string default: '' +permissions: + contents: read jobs: rebuild_releases_page: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/autofix_pr.yml b/.github/workflows/autofix_pr.yml index 9918f6be0fc..1c7b72da68e 100644 --- a/.github/workflows/autofix_pr.yml +++ b/.github/workflows/autofix_pr.yml @@ -13,9 +13,14 @@ on: description: run_clippy type: boolean default: 'true' +permissions: + contents: read jobs: run_autofix: runs-on: namespace-profile-16x32-ubuntu-2204 + permissions: + contents: read + pull-requests: read env: CC: clang CXX: clang++ diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index 4f9724439f3..be39f41b6e5 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -5,10 +5,15 @@ on: # Fire every day at 16:00 UTC (At the start of the US workday) - cron: "0 16 * * *" +permissions: + contents: read + jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' runs-on: namespace-profile-2x4-ubuntu-2404 + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index 3618d7230f7..7be909c907c 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -8,10 +8,14 @@ on: description: Branch name to run on required: true type: string +permissions: + contents: read jobs: run_bump_patch_version: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') runs-on: namespace-profile-16x32-ubuntu-2204 + permissions: + contents: write steps: - id: generate-token name: steps::authenticate_as_zippy diff --git a/.github/workflows/bump_zed_version.yml b/.github/workflows/bump_zed_version.yml index f8fae3de7af..07b17aca32a 100644 --- a/.github/workflows/bump_zed_version.yml +++ b/.github/workflows/bump_zed_version.yml @@ -8,6 +8,8 @@ on: description: 'Which channels to bump: all, main, preview, or stable' type: string default: all +permissions: + contents: read jobs: resolve_versions: if: github.repository_owner == 'zed-industries' @@ -86,6 +88,9 @@ jobs: - resolve_versions if: inputs.target == 'all' || inputs.target == 'main' runs-on: namespace-profile-16x32-ubuntu-2204 + permissions: + contents: write + pull-requests: write steps: - id: generate-token name: steps::authenticate_as_zippy @@ -127,6 +132,8 @@ jobs: - resolve_versions if: inputs.target == 'all' || inputs.target == 'preview' runs-on: namespace-profile-16x32-ubuntu-2204 + permissions: + contents: write steps: - id: generate-token name: steps::authenticate_as_zippy @@ -180,6 +187,8 @@ jobs: - resolve_versions if: inputs.target == 'all' || inputs.target == 'stable' runs-on: namespace-profile-16x32-ubuntu-2204 + permissions: + contents: write steps: - id: generate-token name: steps::authenticate_as_zippy diff --git a/.github/workflows/cherry_pick.yml b/.github/workflows/cherry_pick.yml index 82dc9fb545d..0e18eb2d16d 100644 --- a/.github/workflows/cherry_pick.yml +++ b/.github/workflows/cherry_pick.yml @@ -21,6 +21,8 @@ on: description: pr_number required: true type: string +permissions: + contents: read jobs: run_cherry_pick: runs-on: namespace-profile-2x4-ubuntu-2404 diff --git a/.github/workflows/comment_on_potential_duplicate_issues.yml b/.github/workflows/comment_on_potential_duplicate_issues.yml index 0d7ce3aad3c..c6e79bff848 100644 --- a/.github/workflows/comment_on_potential_duplicate_issues.yml +++ b/.github/workflows/comment_on_potential_duplicate_issues.yml @@ -14,6 +14,8 @@ concurrency: group: potential-duplicate-check-${{ github.event.issue.number || inputs.issue_number }} cancel-in-progress: true +permissions: {} + jobs: identify-duplicates: # For manual testing, allow running on any branch; for automatic runs, only on main repo diff --git a/.github/workflows/community_close_stale_issues.yml b/.github/workflows/community_close_stale_issues.yml index be1d8e66d04..f309dd589b7 100644 --- a/.github/workflows/community_close_stale_issues.yml +++ b/.github/workflows/community_close_stale_issues.yml @@ -13,10 +13,15 @@ on: type: number default: 1000 +permissions: + contents: read + jobs: stale: if: github.repository_owner == 'zed-industries' runs-on: namespace-profile-2x4-ubuntu-2404 + permissions: + issues: write steps: - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10 with: diff --git a/.github/workflows/community_update_all_top_ranking_issues.yml b/.github/workflows/community_update_all_top_ranking_issues.yml index b8003a69b24..55ba214ad30 100644 --- a/.github/workflows/community_update_all_top_ranking_issues.yml +++ b/.github/workflows/community_update_all_top_ranking_issues.yml @@ -5,10 +5,16 @@ on: - cron: "0 */12 * * *" workflow_dispatch: +permissions: + contents: read + jobs: update_top_ranking_issues: runs-on: namespace-profile-2x4-ubuntu-2404 if: github.repository == 'zed-industries/zed' + permissions: + contents: read + issues: write steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up uv diff --git a/.github/workflows/community_update_weekly_top_ranking_issues.yml b/.github/workflows/community_update_weekly_top_ranking_issues.yml index 90d1934ffcb..4dbe5e8700a 100644 --- a/.github/workflows/community_update_weekly_top_ranking_issues.yml +++ b/.github/workflows/community_update_weekly_top_ranking_issues.yml @@ -5,10 +5,16 @@ on: - cron: "0 15 * * *" workflow_dispatch: +permissions: + contents: read + jobs: update_top_ranking_issues: runs-on: namespace-profile-2x4-ubuntu-2404 if: github.repository == 'zed-industries/zed' + permissions: + contents: read + issues: write steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up uv diff --git a/.github/workflows/compliance_check.yml b/.github/workflows/compliance_check.yml index 2cf27fea8b0..a701371d76e 100644 --- a/.github/workflows/compliance_check.yml +++ b/.github/workflows/compliance_check.yml @@ -7,6 +7,8 @@ on: schedule: - cron: 30 17 * * 2 workflow_dispatch: {} +permissions: + contents: read jobs: scheduled_compliance_check: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml index 4866b3c33bc..f6d04265975 100644 --- a/.github/workflows/congrats.yml +++ b/.github/workflows/congrats.yml @@ -4,6 +4,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: check-author: if: ${{ github.repository_owner == 'zed-industries' }} diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 4e94c613a6b..9bef39ade45 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,6 +11,8 @@ on: - edited branches: - main +permissions: + contents: read jobs: danger: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index 043c18ebc7c..b40c55afb14 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -7,6 +7,8 @@ on: push: tags: - collab-production +permissions: + contents: read jobs: style: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 6c492135ea6..ace3c0729e7 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -35,6 +35,8 @@ on: description: Git ref to checkout and deploy. Defaults to event SHA when omitted. type: string default: '' +permissions: + contents: read jobs: deploy_docs: if: github.repository_owner == 'zed-industries' diff --git a/.github/workflows/deploy_nightly_docs.yml b/.github/workflows/deploy_nightly_docs.yml index acd904841bb..12d10f10b26 100644 --- a/.github/workflows/deploy_nightly_docs.yml +++ b/.github/workflows/deploy_nightly_docs.yml @@ -5,6 +5,7 @@ on: push: branches: - main +permissions: {} jobs: deploy_docs: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/docs_suggestions.yml b/.github/workflows/docs_suggestions.yml index c3d04d5780b..f0405af5e94 100644 --- a/.github/workflows/docs_suggestions.yml +++ b/.github/workflows/docs_suggestions.yml @@ -42,6 +42,10 @@ on: - immediate default: batch +# Both jobs below declare their own `permissions:` blocks, so no job uses this +# top-level default. Least privilege therefore grants nothing here. +permissions: {} + env: DROID_MODEL: claude-sonnet-4-5-20250929 SUGGESTIONS_BRANCH: docs/suggestions-pending diff --git a/.github/workflows/extension_auto_bump.yml b/.github/workflows/extension_auto_bump.yml index e48ccdb082a..df9fcc98693 100644 --- a/.github/workflows/extension_auto_bump.yml +++ b/.github/workflows/extension_auto_bump.yml @@ -10,6 +10,8 @@ on: - '!extensions/test-extension/**' - '!extensions/workflows/**' - '!extensions/*.md' +permissions: + contents: read jobs: detect_changed_extensions: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/extension_bump.yml b/.github/workflows/extension_bump.yml index 6e68db7af0f..494a97b0977 100644 --- a/.github/workflows/extension_bump.yml +++ b/.github/workflows/extension_bump.yml @@ -28,6 +28,8 @@ on: app-secret: description: The app secret for the corresponding app ID required: true +permissions: + contents: read jobs: check_version_changed: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/extension_tests.yml b/.github/workflows/extension_tests.yml index 23efa368d17..9a6f223f38f 100644 --- a/.github/workflows/extension_tests.yml +++ b/.github/workflows/extension_tests.yml @@ -15,6 +15,8 @@ on: description: working-directory type: string default: . +permissions: + contents: read jobs: orchestrate: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/extension_workflow_rollout.yml b/.github/workflows/extension_workflow_rollout.yml index 62a9c75d518..d1a47870e6f 100644 --- a/.github/workflows/extension_workflow_rollout.yml +++ b/.github/workflows/extension_workflow_rollout.yml @@ -14,6 +14,8 @@ on: description: Description for the changes to be expected with this rollout type: string default: '' +permissions: + contents: read jobs: fetch_extension_repos: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && (github.ref == 'refs/tags/extension-workflows' || github.ref == 'refs/heads/main') diff --git a/.github/workflows/good_first_issue_notifier.yml b/.github/workflows/good_first_issue_notifier.yml index fc1b49424dc..9e86118dd0a 100644 --- a/.github/workflows/good_first_issue_notifier.yml +++ b/.github/workflows/good_first_issue_notifier.yml @@ -4,6 +4,9 @@ on: issues: types: [labeled] +permissions: + contents: read + jobs: handle-good-first-issue: if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries' diff --git a/.github/workflows/hotfix-review-monitor.yml b/.github/workflows/hotfix-review-monitor.yml index 760cd9806c9..c6e4bfe6e53 100644 --- a/.github/workflows/hotfix-review-monitor.yml +++ b/.github/workflows/hotfix-review-monitor.yml @@ -21,12 +21,13 @@ on: permissions: contents: read - pull-requests: read jobs: check-hotfix-reviews: if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest + permissions: + pull-requests: read timeout-minutes: 5 env: REPO: ${{ github.repository }} diff --git a/.github/workflows/nix_build.yml b/.github/workflows/nix_build.yml index f658634c06c..3ddd6bdd5c6 100644 --- a/.github/workflows/nix_build.yml +++ b/.github/workflows/nix_build.yml @@ -9,6 +9,8 @@ on: types: - labeled - synchronize +permissions: + contents: read jobs: build_nix_linux_x86_64: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && (github.event.label.name == 'run-nix' || github.event.label.name == 'run-bundling')) || (github.event.action == 'synchronize' && (contains(github.event.pull_request.labels.*.name, 'run-nix') || contains(github.event.pull_request.labels.*.name, 'run-bundling')))) diff --git a/.github/workflows/publish_extension_cli.yml b/.github/workflows/publish_extension_cli.yml index b2d8e96fcea..bee3c7c21f5 100644 --- a/.github/workflows/publish_extension_cli.yml +++ b/.github/workflows/publish_extension_cli.yml @@ -11,6 +11,8 @@ on: description: Describe why the extension CLI is being bumped and/or what changes are included. required: true type: string +permissions: + contents: read jobs: publish_job: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.ref == 'refs/heads/main' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8115259c0b7..ec65ab2c872 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,8 @@ on: push: tags: - v* +permissions: + contents: read jobs: run_tests_mac: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') @@ -723,6 +725,8 @@ jobs: - bundle_windows_aarch64 - bundle_windows_x86_64 runs-on: namespace-profile-4x8-ubuntu-2204 + permissions: + contents: write steps: - name: release::download_workflow_artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index e1c72767d77..cf9f4728aa4 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -8,6 +8,8 @@ on: schedule: - cron: 0 */4 * * * workflow_dispatch: {} +permissions: + contents: read jobs: check_nightly_tag: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/run_bundling.yml b/.github/workflows/run_bundling.yml index b05015677c2..4299bac4013 100644 --- a/.github/workflows/run_bundling.yml +++ b/.github/workflows/run_bundling.yml @@ -9,6 +9,8 @@ on: types: - labeled - synchronize +permissions: + contents: read jobs: bundle_linux_aarch64: if: |- diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 3b613f255cc..055279340ce 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -14,6 +14,8 @@ on: branches: - main - v[0-9]+.[0-9]+.x +permissions: + contents: read jobs: orchestrate: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/.github/workflows/slack_notify_community_automation_failure.yml b/.github/workflows/slack_notify_community_automation_failure.yml index ae58a67868d..d02b3b23c23 100644 --- a/.github/workflows/slack_notify_community_automation_failure.yml +++ b/.github/workflows/slack_notify_community_automation_failure.yml @@ -18,6 +18,9 @@ on: - "Guild New PR Notification" types: [completed] +permissions: + contents: read + jobs: notify-slack: if: >- diff --git a/.github/workflows/slack_notify_first_responders.yml b/.github/workflows/slack_notify_first_responders.yml index 3dd9ffeabae..91507ebeefd 100644 --- a/.github/workflows/slack_notify_first_responders.yml +++ b/.github/workflows/slack_notify_first_responders.yml @@ -4,6 +4,9 @@ on: issues: types: [labeled] +permissions: + contents: read + env: PRIORITY_LABELS: '["priority:P0", "priority:P1"]' REPRODUCIBLE_LABEL: 'state:reproducible' diff --git a/.github/workflows/slack_notify_label_created.yml b/.github/workflows/slack_notify_label_created.yml index e791cbc7ea4..f30650dc220 100644 --- a/.github/workflows/slack_notify_label_created.yml +++ b/.github/workflows/slack_notify_label_created.yml @@ -4,6 +4,9 @@ on: label: types: [created] +permissions: + contents: read + jobs: notify-slack: if: >- diff --git a/.github/workflows/stale-pr-reminder.yml b/.github/workflows/stale-pr-reminder.yml index 1c3c0aec623..a35430857ec 100644 --- a/.github/workflows/stale-pr-reminder.yml +++ b/.github/workflows/stale-pr-reminder.yml @@ -20,13 +20,14 @@ on: permissions: contents: read - pull-requests: read jobs: check-stale-prs: if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + pull-requests: read env: REPO: ${{ github.repository }} # Only surface PRs created on or after this date. Update this if the diff --git a/.github/workflows/update_duplicate_magnets.yml b/.github/workflows/update_duplicate_magnets.yml index d14f4aa9245..1d073ba0484 100644 --- a/.github/workflows/update_duplicate_magnets.yml +++ b/.github/workflows/update_duplicate_magnets.yml @@ -5,10 +5,16 @@ on: - cron: "0 6 * * 1,4" # Mondays and Thursdays at 6 AM UTC workflow_dispatch: +permissions: + contents: read + jobs: update-duplicate-magnets: runs-on: ubuntu-latest if: github.repository == 'zed-industries/zed' + permissions: + contents: read + issues: write steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 diff --git a/extensions/workflows/run_tests.yml b/extensions/workflows/run_tests.yml index aa748dc5784..f7e2287700f 100644 --- a/extensions/workflows/run_tests.yml +++ b/extensions/workflows/run_tests.yml @@ -8,6 +8,8 @@ on: push: branches: - main +permissions: + contents: read jobs: call_extension_tests: permissions: diff --git a/extensions/workflows/shared/bump_version.yml b/extensions/workflows/shared/bump_version.yml index dbe92a43a5a..cf4aa1068fa 100644 --- a/extensions/workflows/shared/bump_version.yml +++ b/extensions/workflows/shared/bump_version.yml @@ -11,6 +11,7 @@ on: paths-ignore: - .github/** workflow_dispatch: {} +permissions: {} jobs: determine_bump_type: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') diff --git a/tooling/xtask/src/tasks/workflow_checks.rs b/tooling/xtask/src/tasks/workflow_checks.rs index d6be0299327..2b57c8509cd 100644 --- a/tooling/xtask/src/tasks/workflow_checks.rs +++ b/tooling/xtask/src/tasks/workflow_checks.rs @@ -1,20 +1,22 @@ +mod check_permissions; mod check_run_patterns; -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; -use annotate_snippets::Renderer; +use annotate_snippets::{Group, Renderer}; use anyhow::{Result, anyhow}; use clap::Parser; use itertools::{Either, Itertools}; use serde_yaml::Value; use strum::IntoEnumIterator; -use crate::tasks::{ - workflow_checks::check_run_patterns::{ - RunValidationError, WorkflowFile, WorkflowValidationError, - }, - workflows::WorkflowType, -}; +use crate::tasks::workflows::WorkflowType; + +use check_permissions::PermissionsError; +use check_run_patterns::RunValidationError; pub use check_run_patterns::validate_run_command; @@ -36,14 +38,14 @@ pub fn validate(_: WorkflowValidationArgs) -> Result<()> { parsing_errors.into_iter().join("\n") )) } else if !file_errors.is_empty() { - let errors: Vec<_> = file_errors + let groups: Vec<_> = file_errors .iter() - .map(|error| error.annotation_group()) + .flat_map(|error| error.annotation_groups()) .collect(); let renderer = Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Ascii); - println!("{}", renderer.render(errors.as_slice())); + println!("{}", renderer.render(groups.as_slice())); Err(anyhow!("Workflow checks failed!")) } else { @@ -51,6 +53,61 @@ pub fn validate(_: WorkflowValidationArgs) -> Result<()> { } } +struct WorkflowFile { + raw_content: String, + parsed_content: Value, +} + +impl WorkflowFile { + fn load(workflow_file_path: &Path) -> Result { + fs::read_to_string(workflow_file_path) + .map_err(|_| { + anyhow!( + "Could not read workflow file at {}", + workflow_file_path.display() + ) + }) + .and_then(|file_content| { + serde_yaml::from_str(&file_content) + .map(|parsed_content| Self { + raw_content: file_content, + parsed_content, + }) + .map_err(|e| anyhow!("Failed to parse workflow file: {e:?}")) + }) + } +} + +/// A single kind of validation failure found within a workflow file. +enum ValidationError { + RunInjection(RunValidationError), + Permissions(PermissionsError), +} + +impl ValidationError { + fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> { + match self { + ValidationError::RunInjection(error) => error.annotation_group(file_path, raw_content), + ValidationError::Permissions(error) => error.annotation_group(file_path, raw_content), + } + } +} + +struct WorkflowValidationError { + file_path: PathBuf, + contents: WorkflowFile, + errors: Vec, +} + +impl WorkflowValidationError { + fn annotation_groups(&self) -> Vec> { + self.errors + .iter() + .map(|error| error.annotation_group(&self.file_path, &self.contents.raw_content)) + .collect() + } +} + enum WorkflowError { ParseError(anyhow::Error), ValidationError(Box), @@ -73,41 +130,47 @@ fn get_all_workflow_files() -> impl Iterator { } fn check_workflow(workflow_file_path: PathBuf) -> Result<(), WorkflowError> { - fn collect_errors( - iter: impl Iterator>>, - ) -> Result<(), Vec> { - Some(iter.flat_map(Result::err).flatten().collect::>()) - .filter(|errors| !errors.is_empty()) - .map_or(Ok(()), Err) - } - - fn check_recursive(key: &Value, value: &Value) -> Result<(), Vec> { - match value { - Value::Mapping(mapping) => collect_errors( - mapping - .into_iter() - .map(|(key, value)| check_recursive(key, value)), - ), - Value::Sequence(sequence) => collect_errors( - sequence - .into_iter() - .map(|value| check_recursive(key, value)), - ), - Value::String(string) => check_string(key, string).map_err(|error| vec![error]), - Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Ok(()), - } - } - let file_content = WorkflowFile::load(&workflow_file_path).map_err(WorkflowError::ParseError)?; - check_recursive(&Value::Null, &file_content.parsed_content).map_err(|errors| { - WorkflowError::ValidationError(Box::new(WorkflowValidationError::new( - errors, - file_content, - workflow_file_path, + let mut errors = Vec::new(); + + if let Err(error) = check_permissions::validate_permissions(&file_content.parsed_content) { + errors.push(ValidationError::Permissions(error)); + } + + errors.extend( + collect_run_injection_errors(&Value::Null, &file_content.parsed_content) + .into_iter() + .map(ValidationError::RunInjection), + ); + + if errors.is_empty() { + Ok(()) + } else { + Err(WorkflowError::ValidationError(Box::new( + WorkflowValidationError { + file_path: workflow_file_path, + contents: file_content, + errors, + }, ))) - }) + } +} + +fn collect_run_injection_errors(key: &Value, value: &Value) -> Vec { + match value { + Value::Mapping(mapping) => mapping + .iter() + .flat_map(|(key, value)| collect_run_injection_errors(key, value)) + .collect(), + Value::Sequence(sequence) => sequence + .iter() + .flat_map(|value| collect_run_injection_errors(key, value)) + .collect(), + Value::String(string) => check_string(key, string).err().into_iter().collect(), + Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Vec::new(), + } } fn check_string(key: &Value, value: &str) -> Result<(), RunValidationError> { diff --git a/tooling/xtask/src/tasks/workflow_checks/check_permissions.rs b/tooling/xtask/src/tasks/workflow_checks/check_permissions.rs new file mode 100644 index 00000000000..9fdbc003dde --- /dev/null +++ b/tooling/xtask/src/tasks/workflow_checks/check_permissions.rs @@ -0,0 +1,74 @@ +use annotate_snippets::{AnnotationKind, Group, Level, Snippet}; +use serde_yaml::Value; +use std::{ops::Range, path::Path}; + +pub enum PermissionsError { + /// No top-level `permissions:` key. + Missing, + /// Top-level default grants more than `contents: read`. + ExcessiveDefault, +} + +/// Validates a workflow's top-level (default) `permissions:`. +/// +/// Every workflow must declare one, and it may grant at most `contents: read`; +/// jobs that need more must request it at the job level. +pub fn validate_permissions(workflow: &Value) -> Result<(), PermissionsError> { + let Some(permissions) = workflow.get("permissions") else { + return Err(PermissionsError::Missing); + }; + + let is_minimal = match permissions { + Value::Mapping(mapping) => mapping.iter().all(|(scope, level)| { + let level = level.as_str(); + level == Some("none") || (scope.as_str() == Some("contents") && level == Some("read")) + }), + // String forms such as `read-all`/`write-all` always exceed the allowance. + _ => false, + }; + + if is_minimal { + Ok(()) + } else { + Err(PermissionsError::ExcessiveDefault) + } +} + +impl PermissionsError { + pub fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> { + let (title, span, label) = match self { + PermissionsError::Missing => ( + "Workflow is missing a top-level `permissions:` key", + first_line_span(raw_content, "name:"), + "Add a top-level `permissions:` key so this workflow defaults to the least privilege it needs", + ), + PermissionsError::ExcessiveDefault => ( + "Top-level workflow permissions must grant at most `contents: read`", + first_line_span(raw_content, "permissions:"), + "Lower the default to `contents: read` (or `{}`) and move elevated permissions to the jobs that need them", + ), + }; + + Level::ERROR.primary_title(title).element( + Snippet::source(raw_content) + .path(file_path.display().to_string()) + .annotations(std::iter::once( + AnnotationKind::Primary.span(span).label(label), + )), + ) + } +} + +/// Span of the first column-0 line starting with `prefix`, else empty. +fn first_line_span(raw_content: &str, prefix: &str) -> Range { + let mut offset = 0; + for line in raw_content.lines() { + let start = offset; + offset += line.len() + 1; + if line.starts_with(prefix) { + return start..start + line.len(); + } + } + + Default::default() +} diff --git a/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs b/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs index 50c435d0333..2ea2aea517c 100644 --- a/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs +++ b/tooling/xtask/src/tasks/workflow_checks/check_run_patterns.rs @@ -1,72 +1,25 @@ use annotate_snippets::{AnnotationKind, Group, Level, Snippet}; -use anyhow::{Result, anyhow}; use regex::Regex; -use serde_yaml::Value; -use std::{ - collections::HashMap, - fs, - ops::Range, - path::{Path, PathBuf}, - sync::LazyLock, -}; +use std::{collections::HashMap, ops::Range, path::Path, sync::LazyLock}; static GITHUB_INPUT_PATTERN: LazyLock = LazyLock::new(|| { Regex::new(r#"\$\{\{[[:blank:]]*([[:alnum:]]|[[:punct:]])+?[[:blank:]]*\}\}"#) .expect("Should compile") }); -pub struct WorkflowFile { - raw_content: String, - pub parsed_content: Value, +pub struct RunValidationError { + found_injection_patterns: Vec<(String, Range)>, } -impl WorkflowFile { - pub fn load(workflow_file_path: &Path) -> Result { - fs::read_to_string(workflow_file_path) - .map_err(|_| { - anyhow!( - "Could not read workflow file at {}", - workflow_file_path.display() - ) - }) - .and_then(|file_content| { - serde_yaml::from_str(&file_content) - .map(|parsed_content| Self { - raw_content: file_content, - parsed_content, - }) - .map_err(|e| anyhow!("Failed to parse workflow file: {e:?}")) - }) - } -} - -pub struct WorkflowValidationError { - file_path: PathBuf, - contents: WorkflowFile, - errors: Vec, -} - -impl WorkflowValidationError { - pub fn new( - errors: Vec, - contents: WorkflowFile, - file_path: PathBuf, - ) -> Self { - Self { - file_path, - contents, - errors, - } - } - - pub fn annotation_group<'a>(&'a self) -> Group<'a> { - let raw_content = &self.contents.raw_content; +impl RunValidationError { + /// Renders the GitHub input injection patterns found in a single `run:` + /// command as one diagnostic group. + pub fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> { let mut identical_lines = HashMap::new(); let ranges = self - .errors + .found_injection_patterns .iter() - .flat_map(|error| error.found_injection_patterns.iter()) .map(|(line, pattern_range)| { let initial_offset = identical_lines .get(&(line.as_str(), pattern_range.start)) @@ -89,8 +42,8 @@ impl WorkflowValidationError { Level::ERROR .primary_title("Found GitHub input injection in run command") .element( - Snippet::source(&self.contents.raw_content) - .path(self.file_path.display().to_string()) + Snippet::source(raw_content) + .path(file_path.display().to_string()) .annotations(ranges.map(|range| { AnnotationKind::Primary .span(range) @@ -100,10 +53,6 @@ impl WorkflowValidationError { } } -pub struct RunValidationError { - found_injection_patterns: Vec<(String, Range)>, -} - pub fn validate_run_command(command: &str) -> Result<(), RunValidationError> { let patterns: Vec<_> = command .lines() diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs index 14516c18f22..86a2a952205 100644 --- a/tooling/xtask/src/tasks/workflows.rs +++ b/tooling/xtask/src/tasks/workflows.rs @@ -136,8 +136,8 @@ impl WorkflowFile { .as_ref() .expect("Workflow must have a name at this point"); let filename = format!( - "{}.yml", - workflow_name.rsplit("::").next().unwrap_or(workflow_name) + "{workflow_name}.yml", + workflow_name = workflow_name.rsplit("::").next().unwrap_or(workflow_name) ); let workflow_path = workflow_folder.join(filename); diff --git a/tooling/xtask/src/tasks/workflows/after_release.rs b/tooling/xtask/src/tasks/workflows/after_release.rs index bb363736c34..960d5f24552 100644 --- a/tooling/xtask/src/tasks/workflows/after_release.rs +++ b/tooling/xtask/src/tasks/workflows/after_release.rs @@ -4,7 +4,9 @@ use crate::tasks::workflows::{ deploy_docs::deploy_docs_workflow_call, release::{self, notify_on_failure}, runners, - steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named}, + steps::{ + CommonJobConditions, CommonPermissionSets, NamedJob, checkout_repo, dependant_job, named, + }, vars::{self, StepOutput, WorkflowInput}, }; @@ -39,6 +41,7 @@ pub fn after_release() -> Workflow { }; named::workflow() + .with_minimal_permissions() .add_env(("TAG_NAME", TAG_NAME_ENV)) .add_env(("IS_PRERELEASE", IS_PRERELEASE_ENV)) .on(Event::default() diff --git a/tooling/xtask/src/tasks/workflows/autofix_pr.rs b/tooling/xtask/src/tasks/workflows/autofix_pr.rs index e576f43f91d..60c66459118 100644 --- a/tooling/xtask/src/tasks/workflows/autofix_pr.rs +++ b/tooling/xtask/src/tasks/workflows/autofix_pr.rs @@ -3,8 +3,8 @@ use gh_workflow::*; use crate::tasks::workflows::{ runners, steps::{ - self, DownloadArtifactStep, FluentBuilder, IfNoFilesFound, NamedJob, RepositoryTarget, - TokenPermissions, UploadArtifactStep, ZippyGitIdentity, named, use_clang, + self, CommonPermissionSets, DownloadArtifactStep, FluentBuilder, IfNoFilesFound, NamedJob, + RepositoryTarget, TokenPermissions, UploadArtifactStep, ZippyGitIdentity, named, use_clang, }, vars::{self, StepOutput, WorkflowInput}, }; @@ -15,6 +15,7 @@ pub fn autofix_pr() -> Workflow { let run_autofix = run_autofix(&pr_number, &run_clippy); let commit_changes = commit_changes(&pr_number, &run_autofix); named::workflow() + .with_minimal_permissions() .run_name(format!("autofix PR #{pr_number}")) .on(Event::default().workflow_dispatch( WorkflowDispatch::default() @@ -91,6 +92,11 @@ fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJo named::job(use_clang( Job::default() .runs_on(runners::LINUX_DEFAULT) + .permissions( + Permissions::default() + .contents(Level::Read) + .pull_requests(Level::Read), + ) .outputs([( "has_changes".to_owned(), "${{ steps.create-patch.outputs.has_changes }}".to_owned(), diff --git a/tooling/xtask/src/tasks/workflows/bump_patch_version.rs b/tooling/xtask/src/tasks/workflows/bump_patch_version.rs index fcff7418a25..6b9abab426c 100644 --- a/tooling/xtask/src/tasks/workflows/bump_patch_version.rs +++ b/tooling/xtask/src/tasks/workflows/bump_patch_version.rs @@ -2,7 +2,7 @@ use gh_workflow::*; use crate::tasks::workflows::{ runners, - steps::{self, CheckoutStep, CommonJobConditions, named}, + steps::{self, CheckoutStep, CommonJobConditions, CommonPermissionSets, named}, vars::{StepOutput, WorkflowInput}, }; @@ -10,6 +10,7 @@ pub fn bump_patch_version() -> Workflow { let branch = WorkflowInput::string("branch", None).description("Branch name to run on"); let bump_patch_version_job = run_bump_patch_version(&branch); named::workflow() + .with_minimal_permissions() .on(Event::default() .workflow_dispatch(WorkflowDispatch::default().add_input(branch.name, branch.input()))) .concurrency( @@ -80,6 +81,7 @@ fn run_bump_patch_version(branch: &WorkflowInput) -> steps::NamedJob { named::job( Job::default() .with_repository_owner_guard() + .permissions(Permissions::default().contents(Level::Write)) .runs_on(runners::LINUX_DEFAULT) .add_step(authenticate) .add_step(checkout_branch(branch, &token)) diff --git a/tooling/xtask/src/tasks/workflows/bump_zed_version.rs b/tooling/xtask/src/tasks/workflows/bump_zed_version.rs index 5501412f1be..ccbbd19ee0f 100644 --- a/tooling/xtask/src/tasks/workflows/bump_zed_version.rs +++ b/tooling/xtask/src/tasks/workflows/bump_zed_version.rs @@ -2,7 +2,7 @@ use gh_workflow::*; use crate::tasks::workflows::{ runners, - steps::{self, named}, + steps::{self, CommonPermissionSets, named}, vars::{self, StepOutput, WorkflowInput}, }; @@ -17,6 +17,7 @@ pub fn bump_zed_version() -> Workflow { let stable_job = promote_to_stable(&target, &versions_job, &outputs); named::workflow() + .with_minimal_permissions() .on(Event::default() .workflow_dispatch(WorkflowDispatch::default().add_input(target.name, target.input()))) .add_job(versions_job.name, versions_job.job) @@ -142,6 +143,11 @@ fn bump_main( target.expr(), target.expr(), ))) + .permissions( + Permissions::default() + .contents(Level::Write) + .pull_requests(Level::Write), + ) .needs(vec![versions_job.name.clone()]) .runs_on(runners::LINUX_DEFAULT) .add_step(authenticate) @@ -193,6 +199,7 @@ fn create_preview_branch( target.expr(), target.expr(), ))) + .permissions(Permissions::default().contents(Level::Write)) .needs(vec![versions_job.name.clone()]) .runs_on(runners::LINUX_DEFAULT) .add_step(authenticate) @@ -250,6 +257,7 @@ fn promote_to_stable( target.expr(), target.expr(), ))) + .permissions(Permissions::default().contents(Level::Write)) .needs(vec![versions_job.name.clone()]) .runs_on(runners::LINUX_DEFAULT) .add_step(authenticate) diff --git a/tooling/xtask/src/tasks/workflows/cherry_pick.rs b/tooling/xtask/src/tasks/workflows/cherry_pick.rs index 15d2808dbf6..cc9e8ca64b4 100644 --- a/tooling/xtask/src/tasks/workflows/cherry_pick.rs +++ b/tooling/xtask/src/tasks/workflows/cherry_pick.rs @@ -2,7 +2,10 @@ use gh_workflow::*; use crate::tasks::workflows::{ runners, - steps::{self, NamedJob, RepositoryTarget, TokenPermissions, ZippyGitIdentity, named}, + steps::{ + self, CommonPermissionSets, NamedJob, RepositoryTarget, TokenPermissions, ZippyGitIdentity, + named, + }, vars::{StepOutput, WorkflowInput}, }; @@ -13,6 +16,7 @@ pub fn cherry_pick() -> Workflow { let pr_number = WorkflowInput::string("pr_number", None); let cherry_pick = run_cherry_pick(&branch, &commit, &channel); named::workflow() + .with_minimal_permissions() .run_name(format!("cherry_pick to {channel} #{pr_number}")) .on(Event::default().workflow_dispatch( WorkflowDispatch::default() diff --git a/tooling/xtask/src/tasks/workflows/compliance_check.rs b/tooling/xtask/src/tasks/workflows/compliance_check.rs index 5918bc47677..00f6f1fbe90 100644 --- a/tooling/xtask/src/tasks/workflows/compliance_check.rs +++ b/tooling/xtask/src/tasks/workflows/compliance_check.rs @@ -3,7 +3,7 @@ use gh_workflow::{Event, Job, Schedule, Workflow, WorkflowDispatch}; use crate::tasks::workflows::{ release::{ComplianceContext, add_compliance_steps}, runners, - steps::{self, CommonJobConditions, named}, + steps::{self, CommonJobConditions, CommonPermissionSets, named}, vars::StepOutput, }; @@ -11,6 +11,7 @@ pub fn compliance_check() -> Workflow { let check = scheduled_compliance_check(); named::workflow() + .with_minimal_permissions() .on(Event::default() .schedule([Schedule::new("30 17 * * 2")]) .workflow_dispatch(WorkflowDispatch::default())) diff --git a/tooling/xtask/src/tasks/workflows/danger.rs b/tooling/xtask/src/tasks/workflows/danger.rs index c8e18f6966a..564abf3b30b 100644 --- a/tooling/xtask/src/tasks/workflows/danger.rs +++ b/tooling/xtask/src/tasks/workflows/danger.rs @@ -1,6 +1,6 @@ use gh_workflow::*; -use crate::tasks::workflows::steps::{CommonJobConditions, NamedJob, named}; +use crate::tasks::workflows::steps::{CommonJobConditions, CommonPermissionSets, NamedJob, named}; use super::{runners, steps}; @@ -9,6 +9,7 @@ pub fn danger() -> Workflow { let danger = danger_job(); named::workflow() + .with_minimal_permissions() .on(Event::default() .pull_request(PullRequest::default().add_branch("main").types([ PullRequestType::Opened, diff --git a/tooling/xtask/src/tasks/workflows/deploy_collab.rs b/tooling/xtask/src/tasks/workflows/deploy_collab.rs index e372a27d237..5302bc2f3ff 100644 --- a/tooling/xtask/src/tasks/workflows/deploy_collab.rs +++ b/tooling/xtask/src/tasks/workflows/deploy_collab.rs @@ -3,7 +3,8 @@ use indoc::indoc; use crate::tasks::workflows::runners::{self, Platform}; use crate::tasks::workflows::steps::{ - self, CommonJobConditions, FluentBuilder as _, NamedJob, dependant_job, named, use_clang, + self, CommonJobConditions, CommonPermissionSets, FluentBuilder as _, NamedJob, dependant_job, + named, use_clang, }; use crate::tasks::workflows::vars; @@ -14,6 +15,7 @@ pub(crate) fn deploy_collab() -> Workflow { let deploy = deploy(&[&publish]); named::workflow() + .with_minimal_permissions() .on(Event::default().push(Push::default().add_tag("collab-production"))) .add_env(("DOCKER_BUILDKIT", "1")) .add_job(style.name, style.job) diff --git a/tooling/xtask/src/tasks/workflows/deploy_docs.rs b/tooling/xtask/src/tasks/workflows/deploy_docs.rs index f60d1097d6e..7871022f770 100644 --- a/tooling/xtask/src/tasks/workflows/deploy_docs.rs +++ b/tooling/xtask/src/tasks/workflows/deploy_docs.rs @@ -6,8 +6,8 @@ use gh_workflow::{ use crate::tasks::workflows::{ runners, steps::{ - self, CommonJobConditions, FluentBuilder as _, NamedJob, UploadArtifactStep, named, - release_job, + self, CommonJobConditions, CommonPermissionSets, FluentBuilder as _, NamedJob, + UploadArtifactStep, named, release_job, }, vars::{self, StepOutput, WorkflowInput}, }; @@ -314,6 +314,7 @@ pub(crate) fn deploy_docs() -> Workflow { let deploy_docs = deploy_docs_job(&channel, &checkout_ref); named::workflow() + .with_minimal_permissions() .add_event( Event::default().workflow_dispatch( WorkflowDispatch::default() @@ -366,6 +367,7 @@ pub(crate) fn deploy_nightly_docs() -> Workflow { named::workflow() .name("deploy_nightly_docs") + .permissions(Permissions::default()) .add_event(Event::default().push(Push::default().add_branch("main"))) .add_job(deploy_docs.name, deploy_docs.job) } diff --git a/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs b/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs index e69c783299a..9958a7638d6 100644 --- a/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs +++ b/tooling/xtask/src/tasks/workflows/extension_auto_bump.rs @@ -8,7 +8,7 @@ use crate::tasks::workflows::{ extensions::WithAppSecrets, run_tests::DETECT_CHANGED_EXTENSIONS_SCRIPT, runners, - steps::{self, CommonJobConditions, NamedJob, named}, + steps::{self, CommonJobConditions, CommonPermissionSets, NamedJob, named}, vars::{StepOutput, one_workflow_per_non_main_branch}, }; @@ -20,6 +20,7 @@ pub(crate) fn extension_auto_bump() -> Workflow { let bump = bump_extension_versions(&detect); named::workflow() + .with_minimal_permissions() .add_event( Event::default().push( Push::default() diff --git a/tooling/xtask/src/tasks/workflows/extension_bump.rs b/tooling/xtask/src/tasks/workflows/extension_bump.rs index e4efebfe67d..9f6f6462b6c 100644 --- a/tooling/xtask/src/tasks/workflows/extension_bump.rs +++ b/tooling/xtask/src/tasks/workflows/extension_bump.rs @@ -5,9 +5,10 @@ use crate::tasks::workflows::{ extension_tests::{self}, runners, steps::{ - self, BASH_SHELL, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, GitHubScriptStep, - GitRef, NamedJob, RefSha, RepositoryTarget, cache_rust_dependencies_namespace, - checkout_repo, create_ref, dependant_job, generate_token, named, + self, BASH_SHELL, CommonJobConditions, CommonPermissionSets, + DEFAULT_REPOSITORY_OWNER_GUARD, GitHubScriptStep, GitRef, NamedJob, RefSha, + RepositoryTarget, cache_rust_dependencies_namespace, checkout_repo, create_ref, + dependant_job, generate_token, named, }, vars::{ JobOutput, StepOutput, WorkflowInput, WorkflowSecret, @@ -58,6 +59,7 @@ pub(crate) fn extension_bump() -> Workflow { ); named::workflow() + .with_minimal_permissions() .add_event( Event::default().workflow_call( WorkflowCall::default() diff --git a/tooling/xtask/src/tasks/workflows/extension_tests.rs b/tooling/xtask/src/tasks/workflows/extension_tests.rs index b184db64e3f..8626dd5e1c0 100644 --- a/tooling/xtask/src/tasks/workflows/extension_tests.rs +++ b/tooling/xtask/src/tasks/workflows/extension_tests.rs @@ -8,7 +8,7 @@ use crate::tasks::workflows::{ }, runners, steps::{ - self, BASH_SHELL, CommonJobConditions, FluentBuilder, NamedJob, + self, BASH_SHELL, CommonJobConditions, CommonPermissionSets, FluentBuilder, NamedJob, cache_rust_dependencies_namespace, named, }, vars::{PathCondition, StepOutput, WorkflowInput, one_workflow_per_non_main_branch_and_token}, @@ -41,6 +41,7 @@ pub(crate) fn extension_tests() -> Workflow { let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned())); named::workflow() + .with_minimal_permissions() .add_event( Event::default().workflow_call( WorkflowCall::default() diff --git a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs index 9eb9d456a62..19e2c5e28db 100644 --- a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs +++ b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs @@ -8,8 +8,8 @@ use serde_json::json; use crate::tasks::workflows::steps::GitRef; use crate::tasks::workflows::steps::RefSha; use crate::tasks::workflows::steps::{ - CheckoutStep, DownloadArtifactStep, IfNoFilesFound, ResultEncoding, TokenPermissions, - UploadArtifactStep, cache_rust_dependencies_namespace, + CheckoutStep, CommonPermissionSets, DownloadArtifactStep, IfNoFilesFound, ResultEncoding, + TokenPermissions, UploadArtifactStep, cache_rust_dependencies_namespace, }; use crate::tasks::workflows::vars::JobOutput; use crate::tasks::workflows::{ @@ -42,6 +42,7 @@ pub(crate) fn extension_workflow_rollout() -> Workflow { let create_tag = create_rollout_tag(&rollout_workflows, &filter_repos_input); named::workflow() + .with_minimal_permissions() .on(Event::default().workflow_dispatch( WorkflowDispatch::default() .add_input(filter_repos_input.name, filter_repos_input.input()) diff --git a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs index 4dc2560e2be..077a0e4cc20 100644 --- a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs +++ b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs @@ -27,6 +27,7 @@ pub(crate) fn bump_version(args: &GenerateWorkflowArgs) -> Workflow { ) .pull_request(PullRequest::default().add_type(PullRequestType::Labeled)) .workflow_dispatch(WorkflowDispatch::default())) + .permissions(Permissions::default()) .concurrency(one_workflow_per_non_main_branch_and_token("labels")) .add_job(determine_bump_type.name, determine_bump_type.job) .add_job(call_bump_version.name, call_bump_version.job) diff --git a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs index ae8000c15ca..3bcd87c433e 100644 --- a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs @@ -9,6 +9,7 @@ use crate::tasks::workflows::{ pub(crate) fn run_tests(args: &GenerateWorkflowArgs) -> Workflow { let call_extension_tests = call_extension_tests(args.sha.as_ref()); named::workflow() + .permissions(Permissions::default().contents(Level::Read)) .on(Event::default() .pull_request(PullRequest::default().add_branch("**")) .push(Push::default().add_branch("main"))) diff --git a/tooling/xtask/src/tasks/workflows/nix_build.rs b/tooling/xtask/src/tasks/workflows/nix_build.rs index cc04ec5b855..2a4521ecdc4 100644 --- a/tooling/xtask/src/tasks/workflows/nix_build.rs +++ b/tooling/xtask/src/tasks/workflows/nix_build.rs @@ -1,6 +1,6 @@ use crate::tasks::workflows::{ runners::{Arch, Platform}, - steps::{CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob}, + steps::{CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob}, }; use super::{runners, steps, steps::named, vars}; @@ -13,6 +13,7 @@ use gh_workflow::*; pub fn nix_build() -> Workflow { let [nix_linux_x86_64, nix_mac_aarch64] = nix_pr_jobs(&["run-nix", "run-bundling"]); named::workflow() + .with_minimal_permissions() .on(Event::default().pull_request( PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), )) diff --git a/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs b/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs index cbe656e734e..d4843cbaf4a 100644 --- a/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs +++ b/tooling/xtask/src/tasks/workflows/publish_extension_cli.rs @@ -4,8 +4,8 @@ use indoc::{formatdoc, indoc}; use crate::tasks::workflows::{ runners, steps::{ - self, DEFAULT_REPOSITORY_OWNER_GUARD, GitRef, NamedJob, RefSha, RepositoryTarget, - TokenPermissions, generate_token, named, + self, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, GitRef, NamedJob, RefSha, + RepositoryTarget, TokenPermissions, generate_token, named, }, vars::{self, StepOutput, WorkflowInput}, }; @@ -22,6 +22,7 @@ pub fn publish_extension_cli() -> Workflow { let update_sha_in_extensions = update_sha_in_extensions(&publish, &message); named::workflow() + .with_minimal_permissions() .on(Event::default().workflow_dispatch( WorkflowDispatch::default().add_input(message.name, message.input()), )) diff --git a/tooling/xtask/src/tasks/workflows/release.rs b/tooling/xtask/src/tasks/workflows/release.rs index f04a9f7ca8a..fc3060e375a 100644 --- a/tooling/xtask/src/tasks/workflows/release.rs +++ b/tooling/xtask/src/tasks/workflows/release.rs @@ -1,4 +1,6 @@ -use gh_workflow::{Event, Expression, Level, Push, Run, Step, Use, Workflow, ctx::Context}; +use gh_workflow::{ + Event, Expression, Level, Permissions, Push, Run, Step, Use, Workflow, ctx::Context, +}; use indoc::formatdoc; use crate::tasks::workflows::{ @@ -6,8 +8,8 @@ use crate::tasks::workflows::{ run_tests, runners::{self, Arch, Platform}, steps::{ - self, DownloadArtifactStep, FluentBuilder, NamedJob, TokenPermissions, dependant_job, - named, release_job, + self, CommonPermissionSets, DownloadArtifactStep, FluentBuilder, NamedJob, + TokenPermissions, dependant_job, named, release_job, }, vars::{self, JobOutput, StepOutput, assets}, }; @@ -101,6 +103,7 @@ pub(crate) fn release() -> Workflow { named::workflow() .on(Event::default().push(Push::default().tags(vec!["v*".to_string()]))) .concurrency(vars::one_workflow_per_non_main_branch()) + .with_minimal_permissions() .add_env(("CARGO_TERM_COLOR", "always")) .add_env(("RUST_BACKTRACE", "1")) .add_job(macos_tests.name, macos_tests.job) @@ -466,6 +469,7 @@ fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> Name named::job( dependant_job(&deps) .runs_on(runners::LINUX_MEDIUM) + .permissions(Permissions::default().contents(Level::Write)) .add_step(download_workflow_artifacts()) .add_step(steps::script("ls -lR ./artifacts")) .add_step(prep_release_artifacts()) diff --git a/tooling/xtask/src/tasks/workflows/release_nightly.rs b/tooling/xtask/src/tasks/workflows/release_nightly.rs index a92188ee0d7..4c18ba21368 100644 --- a/tooling/xtask/src/tasks/workflows/release_nightly.rs +++ b/tooling/xtask/src/tasks/workflows/release_nightly.rs @@ -8,8 +8,8 @@ use crate::tasks::workflows::{ run_tests::run_platform_tests_no_filter, runners::{Arch, Platform, ReleaseChannel}, steps::{ - CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, GitRef, NamedJob, - RefSha, RepositoryTarget, TokenPermissions, + CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, + GitRef, NamedJob, RefSha, RepositoryTarget, TokenPermissions, }, }; @@ -47,6 +47,7 @@ pub fn release_nightly() -> Workflow { let notify_on_failure = notify_on_failure(&bundle.jobs()); named::workflow() + .with_minimal_permissions() .on(Event::default() // Fire 6 times a day .schedule([Schedule::new("0 */4 * * *")]) diff --git a/tooling/xtask/src/tasks/workflows/run_bundling.rs b/tooling/xtask/src/tasks/workflows/run_bundling.rs index e9ed32c610c..cb916e849ed 100644 --- a/tooling/xtask/src/tasks/workflows/run_bundling.rs +++ b/tooling/xtask/src/tasks/workflows/run_bundling.rs @@ -3,7 +3,10 @@ use std::path::Path; use crate::tasks::workflows::{ release::ReleaseBundleJobs, runners::{Arch, Platform, ReleaseChannel}, - steps::{FluentBuilder, IfNoFilesFound, NamedJob, UploadArtifactStep, dependant_job, named}, + steps::{ + CommonPermissionSets, FluentBuilder, IfNoFilesFound, NamedJob, UploadArtifactStep, + dependant_job, named, + }, vars::{self, assets, bundle_envs}, }; @@ -23,6 +26,7 @@ pub fn run_bundling() -> Workflow { windows_x86_64: bundle_windows(Arch::X86_64, None, &[]), }; named::workflow() + .with_minimal_permissions() .on(Event::default().pull_request( PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), )) diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index dd422fa32e9..c745d0e3852 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -8,8 +8,8 @@ use serde_json::json; use crate::tasks::workflows::{ steps::{ - CommonJobConditions, cache_rust_dependencies_namespace, repository_owner_guard_expression, - use_clang, + CommonJobConditions, CommonPermissionSets, cache_rust_dependencies_namespace, + repository_owner_guard_expression, use_clang, }, vars::{self, PathCondition}, }; @@ -105,6 +105,7 @@ pub(crate) fn run_tests() -> Workflow { ); // could be more specific here? named::workflow() + .with_minimal_permissions() .add_event( Event::default() .push( diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs index 7351f451a94..5fcb0114421 100644 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ b/tooling/xtask/src/tasks/workflows/steps.rs @@ -376,6 +376,16 @@ impl CommonJobConditions for Job { } } +pub trait CommonPermissionSets: Sized { + fn with_minimal_permissions(self) -> Self; +} + +impl CommonPermissionSets for Workflow { + fn with_minimal_permissions(self) -> Self { + self.permissions(Permissions::default().contents(Level::Read)) + } +} + pub(crate) fn release_job(deps: &[&NamedJob]) -> Job { dependant_job(deps) .with_repository_owner_guard() @@ -503,6 +513,7 @@ pub mod named { .collect::>() .join("::"), ) + .permissions(Permissions::default()) .defaults(Defaults::default().run(RunDefaults::default().shell(BASH_SHELL))) }