Draft: Rd summit challenge policy store [ci skip]

What does this MR do and why?

Builds the Policy Store v2 authoring stack on the branch, derived from the deployment-gate PoC in !245588:

  • gitlab-security-policy-store gem (Gitlab::SecurityPolicyStore) — AR-free store with a read/write seam (Store#load_policies/#list/#find/#create/#delete) over an injected adapter, plus the ScopeTranspiler.
  • Persistence — the policies table in the sec schema (gitlab_sec), Security::Policies::V2::Policy, and Security::Policies::V2::StoreAdapter. The cross-schema link from protected_environment_approval_rules is a loose foreign key.
  • Authoring servicesSecurity::SecurityOrchestrationPolicies::PolicyStore::{Create,Find,List,Destroy}, routed through the store gem.
  • REST API — Full CRUD for policies:
    • POST /groups/:id/security/policies (create)
    • GET /groups/:id/security/policies (list)
    • GET /groups/:id/security/policies/:policy_id (show)
    • DELETE /groups/:id/security/policies/:policy_id (delete)
    • Plus metadata lists: GET /security/policies/{triggers,actions,rules}
    • All gated by the Policy Store experiment.
  • Deployment gateEE::Ci::ProcessBuildService lists deployment policies through the store (scoped to the project's organization) and evaluates them.

The Rego evaluator (Gitlab::Glaz.evaluate_rego) is owned by the engine team and is not yet on this branch, so end-to-end gate enforcement is not runnable here — the gate's unit spec stubs the evaluator. Authoring via the REST API is fully runnable.

References

  • Derived from the deployment-gate PoC: !245588
  • Design: UNIFIED_POLICY_STORE_GEM_DESIGN.md, SECURITY_POLICY_V2_CREATE_MUTATION_PLAN.md, POLICY_STORE_DEPLOYMENT_GATE_INTEGRATION_PLAN.md

How to verify (REST API)

Backend/console + curl. This replaces the PoC's security_policies:v2:seed rake task with the REST authoring API.

One-time setup

bundle install
bundle exec rails db:migrate

Enable the Policy Store experiment

# rails console — the experiment is required for authoring + the gate
ApplicationSetting.current.update!(policy_store_experiment_enabled: true)   # instance

group = Group.find_by_full_path("your-group")
group.namespace_settings.update!(policy_store_experiment_enabled: true)     # group

Feature.enable(:security_policies_v2, group)                                # feature flag
# Ultimate license (security_orchestration_policies) must be present.

Create a policy

TOKEN=<personal access token with api scope>
GITLAB_URL=http://gdk.test:3000

curl --request POST \
  --header "PRIVATE-TOKEN: $TOKEN" \
  --header "Content-Type: application/json" \
  --url "$GITLAB_URL/api/v4/groups/your-group/security/policies" \
  --data '{"name": "Production deployment gate", "trigger_id": "deployment_requested", "mode": "enforce", "rules": { "rego": "package governance\nimport rego.v1\n\nmatches contains msg if {\n  input.environment.name == \"production\"\n  msg := \"Production deployment blocked\"\n}" }, "actions": [{ "type": "block", "reason": "Enforced by policy" }]}'
# => 201 Created, returns the policy (id, name, trigger_id, mode, scope_rego)

List policies for a group

curl --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/groups/your-group/security/policies"
# => [{ "id": 1, "name": "Production deployment gate", ... }]

Get a single policy by ID

curl --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/groups/your-group/security/policies/1"
# => { "id": 1, "name": "Production deployment gate", ... }

Delete a policy

curl --request DELETE --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/groups/your-group/security/policies/1"
# => 204 No Content

List authoring metadata

curl --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/security/policies/triggers"
# => [{ "id": "deployment_requested", "name": "Deployment" }]
curl --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/security/policies/actions"
# => block, require_approval
curl --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB_URL/api/v4/security/policies/rules"
# => custom, calendar, environment

Verify persistence

# rails console — the row lands in the sec schema
policy = Security::Policies::V2::Policy.last
policy.slice(:organization_id, :trigger_id, :mode)
policy.rules      # { "rego" => "package governance..." }
policy.actions    # [{ "type" => "block", ... }]

Negative checks: with the experiment disabled (either the instance or group setting), the endpoints return 404; without the Ultimate license they return 403; an unknown trigger_id returns 400; requesting a non-existent policy ID returns 404.

Deployment gate (pending the evaluator)

The gate (EE::Ci::ProcessBuildService) lists these policies through the store and evaluates each rego-authored rule, enforcing block/log actions honoring mode. Because Gitlab::Glaz.evaluate_rego is not yet on the branch, this path can't be exercised end-to-end here; the expected behavior (covered by the gate spec) is:

Policy deploy-prod job
enforce + block, rego matches failed, reason security_policy_denied
audit mode proceeds
no matching policy / experiment off proceeds
typed rules (no rego) skipped (rego-only for now)

Deferred

Scope (scope_rego) applicability filtering, typed-rule evaluation, the require_approval executor, an org-scoped store query, and the external Rego evaluator/engine gem.

Edited by Andy Schoenen

Merge request reports

Loading
Loading