Serve a combined per-policy Rego module from the REST API

What does this MR do and why?

This MR adds a policy_rego attribute to every Policy Store REST response: a single Rego module that merges all of a policy's rules under one package governance header, in authored order. It appears on the list, retrieve, create, and update endpoints. Each rule's own program stays available under rules[].rego; policy_rego is purely additive, and null for a policy with no rules. No request parameters changed on any endpoint.

The Policy Engine evaluates one program per policy at roughly 25 to 30% lower cost than one program per rule, a difference measured in review of !249000 (merged) (merged). The downstream consumer, glaz_govern::Policy, already expects that combined shape as a single policy_rego string per policy, so serving it from the REST API lets a REST-backed lookup map onto it directly, with no merge logic reimplemented downstream.

Design decisions

  • Extract a shared RegoPackage helper. Gitlab::PolicyStore::RuleTranspiler already had logic to find a Rego program's declared package line past leading comments and a trailing # comment. The merge needs that same logic to strip each rule's package line before concatenating bodies, so it is extracted into a reusable module instead of duplicated, and RuleTranspiler now calls it too.
  • Compute the merged program at render time, not store it. Per-rule rows stay the single source of truth, and the merge is cheap enough that caching it would be premature. No change is needed to Gitlab::PolicyStore::Policy or to either write endpoint.
  • Name the field policy_rego, to match glaz_govern::Policy. That Rust struct lives outside this monorepo, so the name could not be verified against its actual proto from here. Flagging this to reviewers as unverified rather than assuming it is correct.
  • Rescue a rule missing its compiled rego at render time. Without it, one bad policy would fail the whole list response for its organization rather than only itself, so the entity tracks the error and renders policy_rego as nil for that policy alone.

How to set up and validate locally

  1. Enable the feature flag and the application setting on the rails console
Feature.enable(:security_policies_v2)
ApplicationSetting.current.update!(policy_store_experiment_enabled: true)
  1. Assert they took effect, and that the current user has an Ultimate licence, on the same console
Feature.enabled?(:security_policies_v2, :instance)             # => true
Gitlab::CurrentSettings.policy_store_experiment_enabled?       # => true
License.feature_available?(:security_orchestration_policies)   # => true
  1. Get a personal access token with the api scope, for a user who is an administrator or an owner of the organization. Organization id 1 is the default on a GDK. Create a policy carrying two rules
curl --request POST \
  --url "https://gdk.test:3443/api/v4/organizations/1/security/policy_store" \
  --header "PRIVATE-TOKEN: <your_access_token>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Combined module check",
    "trigger_type": "deployment_requested",
    "rules": [
      { "type": "environment", "value": { "tiers": ["production"] } },
      { "type": "custom", "value": "package governance\n\nviolation contains {\"msg\": \"no\"}\n" }
    ]
  }' \
  --write-out '\nHTTP %{http_code}\n'

Verify this answers 201, and keep the returned id for the next steps, because the response body carries the policy_rego this MR adds.

  1. Read policy_rego from that response

Verify it holds one package governance header carrying both rules' bodies, each still under its own # rule N: <type> header, and that rules[0].rego and rules[1].rego still carry each rule's own program unchanged, because the merged field is additive rather than a replacement for the per-rule detail.

Example

{
  "id": 20003,
  "organization_id": 1,
  "namespace_id": null,
  "name": "Combined module check",
  "description": null,
  "version": 1,
  "trigger_type": "deployment_requested",
  "rules": [
    {
      "rego": "package governance\n\n# rule 0: environment\n\nviolation contains {\"msg\": msg, \"details\": {\"rule_index\": 0, \"environment_id\": input.environment.id, \"environment_name\": input.environment.name}} if {\n\tinput.environment.tier in {\"production\"}\n\tmsg := sprintf(\"deployment to %s is blocked by this policy\", [input.environment.name])\n}\n",
      "type": "environment",
      "value": {
        "tiers": ["production"]
      }
    },
    {
      "rego": "package governance\n\nviolation contains {\"msg\": \"no\"}\n",
      "type": "custom",
      "value": "package governance\n\nviolation contains {\"msg\": \"no\"}\n"
    }
  ],
  "policy_rego": "package governance\n\n# rule 0: environment\n\nviolation contains {\"msg\": msg, \"details\": {\"rule_index\": 0, \"environment_id\": input.environment.id, \"environment_name\": input.environment.name}} if {\n\tinput.environment.tier in {\"production\"}\n\tmsg := sprintf(\"deployment to %s is blocked by this policy\", [input.environment.name])\n}\n\nviolation contains {\"msg\": \"no\"}\n",
  "actions": [],
  "policy_scope": null,
  "scope_rego": "package gitlab.scope\n\n# policy \"Combined module check\"\n# no policy_scope: applies to all projects\napplies := true\n",
  "mode": "enforce",
  "lifecycle_state": "active",
  "created_at": "2026-08-19T18:33:16.661Z",
  "updated_at": "2026-08-19T18:33:16.661Z"
}

References

Edited by Marcos Rocha

Merge request reports

Loading
Loading