Persist policy evaluations and violations through the Policy Store

What does this MR do and why?

MR 2 of https://gitlab.com/gitlab-org/gitlab/-/work_items/607735 (Policy Store: store policy evaluations + violations). !250860 (merged) (merged) added the govern_policy_evaluations and govern_policy_violations tables and models. This MR adds the write path that persists into them. No migrations here.

No user-facing change and no changelog: everything sits behind the default-off security_policies_v2 feature flag plus the policy store experiment settings, and nothing calls this code yet. The caller will be the Policy Engine (regorus embedded in gitlab-glaz), which is still in prototype.

On design, this deliberately mirrors the existing policy repository port/adapter pattern rather than adding an HTTP internal endpoint. Per the interface constraints in https://gitlab.com/gitlab-org/gitlab/-/work_items/604407, the engine must never touch ActiveRecord directly, everything goes through an injectable port so the in-monolith backend can later be swapped for a remote adapter (e.g. gRPC) without touching the engine or its callers. Recording is a separate port from the policy repository since it serves a different caller (the engine reporting a completed evaluation) than the management surfaces that edit policies.

Three commits, meant to be reviewed independently and in order:

  1. Add evaluation recorder port to the policy store gem (+462/-12) - in gems/gitlab-policy-store: Gitlab::PolicyStore::Ports::EvaluationRecorder validates required attributes, trigger_type/mode/verdict enum membership, a positive policy_version, and violation entry shape (each entry only allows details). Adds frozen Evaluation and Violation value objects, an Adapters::InMemoryEvaluationRecorder default, a new evaluation_recorder slot on Configuration, and a Gitlab::PolicyStore.record_evaluation(attributes) facade. The port takes policy_id, not govern_policy_id, DB naming stays out of the contract on purpose. Shared examples 'an evaluation recorder' exercise the contract so both adapters run the same tests.
  2. Validate Govern policy violation details jsonb (+37/-1) - bounded JsonSchemaValidator on Govern::PolicyViolation#details, 64 KiB size limit, permissive object schema (govern_policy_violation_details.json), and the column comes out of jsonb_column_validation_todo.yml. This was the database review condition on MR 1, bounded JSON validation had to land before the write path could accept payloads into this column. The schema stays permissive on purpose since the Policy Engine's decision format isn't final yet, the size limit is the bound that actually matters right now.
  3. Back the evaluation recorder with the Govern tables (+151) - Govern::PolicyStore::ActiveRecordEvaluationRecorder in ee/lib, wired into the existing EE-only initializer next to the policy repository (FOSS keeps the gem's inert in-memory default). Writes the evaluation and its violations in one transaction, so a rejected violation leaves no evaluation row behind. Runs the same gem shared examples against the real tables, plus AR-specific cases for atomicity, a nonexistent policy, and an organization mismatch.

One thing I'd like Policy Engine folks to weigh in on: the recorder's input shape (the attributes hash) is meant to match the engine's structured decision format, but that format isn't finalized. If it shifts, the port contract may need an expand/contract pass. Flagging this now so we're not surprised later. WDYT?

References

How to set up and validate locally

Nothing calls this code path yet, so there's no UI flow to click through. In a Rails console on EE:

policy = Gitlab::PolicyStore.create(organization_id: Organizations::Organization.first.id,
  name: 'Demo policy', trigger_type: 'deployment_requested')

Gitlab::PolicyStore.record_evaluation(
  organization_id: policy.organization_id, policy_id: policy.id, policy_version: policy.version,
  trigger_type: 'deployment_requested', mode: 'audit', verdict: 'deny', evaluated_at: Time.current,
  violations: [{ details: { 'reason' => 'demo' } }]
)

This returns a frozen Gitlab::PolicyStore::Evaluation and persists one Govern::PolicyEvaluation row with one Govern::PolicyViolation row.

Specs:

(cd gems/gitlab-policy-store && bundle exec rspec)
bundle exec rspec ee/spec/lib/govern/policy_store ee/spec/models/govern/policy_violation_spec.rb ee/spec/initializers/policy_store_spec.rb

Gem suite: 287 examples, green. EE adapter + initializer specs: 156 examples, green. Model spec: 12 examples, green. Gem and monolith RuboCop are clean.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading
Loading