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_bypassedboolean toci_pipeline_metadata(defaults tofalse) - Adds the matching column to the
siphon_ci_pipeline_metadataClickHouse table, since the Postgres table is replicated via Siphon Chain::Validate::Abilitiesrecords the decision on the chain command, only when the bypass was decisive (the regular permission check failed)Chain::PopulateMetadatapersists 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
- Follow-up issue: #602327
- Original discussion: !238818 (merged)
How to set up and validate locally
-
Create a test project with a trivial
.gitlab-ci.ymlso pushes create pipelines:test-job: script: - echo ok -
Create a project access token with the Developer role and
api, write_repositoryscopes (Settings > Access tokens). Copy the token value and look up its ID in the Rails console:PersonalAccessToken.find_by(name: '<token name>').id -
Protect the default branch with Allowed to push and merge: Maintainers (Settings > Repository > Protected branches). The Developer bot can't push now.
-
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 -
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 mainThe push is only allowed because of the bypass, and it creates a push pipeline.
-
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 -
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_metadataisnil, no metadata row gets created. - Push to an unprotected branch with the token:
pipeline.pipeline_metadatais alsonil.
- Push to the protected branch as a user who's allowed to push (for example the Owner):
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.