Record approval policy bypass decision on pipeline metadata

What does this MR do and why?

Records the approval policy bypass decision with the pipeline, addressing the first follow-up from !238818 (merged): the decision depends on push options, which are gone once the pipeline exists, so consumers that run later (child pipelines, manual and delayed jobs) cannot re-evaluate it.

  • Adds a security_policy_protected_branch_bypassed boolean to ci_pipeline_metadata (defaults to false)
  • Adds the matching column to the siphon_ci_pipeline_metadata ClickHouse table, since the Postgres table is replicated via Siphon
  • Chain::Validate::Abilities records the decision on the chain command, only when the bypass was decisive (the regular permission check failed)
  • Chain::PopulateMetadata persists it on the pipeline metadata

Write path only, no behavior change. Consumers of the recorded decision come in later iterations.

Why the ClickHouse change as part of this MR: I first split it into !246929 (closed), but spec/db/clickhouse_siphon_tables_spec.rb enforces column parity between Siphon-replicated Postgres tables and their ClickHouse counterparts within the same revision, which failed the pipeline.

References

How to set up and validate locally

  1. Create a test project with a trivial .gitlab-ci.yml so pushes create pipelines:

    test-job:
      script:
        - echo ok
  2. Create a project access token with the Developer role and api, write_repository scopes (Settings > Access tokens). Copy the token value and look up its ID in the Rails console:

    PersonalAccessToken.find_by(name: '<token name>').id
  3. Protect the default branch with Allowed to push and merge: Maintainers (Settings > Repository > Protected branches). The Developer bot can't push now.

  4. Create a merge request approval policy with a token bypass (Secure > Policies > New policy > Merge request approval policy, switch to .yaml mode):

    approval_policy:
      - name: Token bypass verify
        enabled: true
        rules:
          - type: any_merge_request
            branch_type: protected
            commits: any
        actions:
          - type: require_approval
            approvals_required: 1
            role_approvers:
              - owner
        bypass_settings:
          access_tokens:
            - id: <token ID from step 2>

    Save with Configure with a merge request and merge the generated MR in the policy project. Confirm the sync in the console before moving on:

    Project.find_by_full_path('<project path>').security_policies.with_bypass_settings
    # => the policy, with bypass_settings.access_tokens containing your token ID
  5. Clone the project with the token and push to the protected branch as the bot:

    git clone "http://<bot username>:<token value>@gdk.test:3000/<project path>.git"
    cd <project name>
    git commit --allow-empty -m "Bypass push"
    git push origin main

    The push is only allowed because of the bypass, and it creates a push pipeline.

  6. Check the recorded decision:

    pipeline = Project.find_by_full_path('<project path>').ci_pipelines.last
    pipeline.user     # => the token bot
    pipeline.source   # => "push"
    pipeline.pipeline_metadata.security_policy_protected_branch_bypassed
    # => true
  7. Control cases, where the flag must stay unset because the bypass wasn't decisive:

    • Push to the protected branch as a user who's allowed to push (for example the Owner): pipeline.pipeline_metadata is nil, no metadata row gets created.
    • Push to an unprotected branch with the token: pipeline.pipeline_metadata is also nil.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Dominic Bauer

Merge request reports

Loading