Add Policy Store v2 authoring services
What does this MR do and why?
Adds the Policy Store v2 authoring services, ported from the R&D experiment
(!246660 (closed)) and
retargeted at the Gitlab::PolicyStore facade:
Security::SecurityOrchestrationPolicies::PolicyStore::{Create,Find,List,Destroy}Service, over a sharedBaseServicethat holds the experiment gate and the organization scopingGitlab::PolicyStore.reset_configuration!in the gem, for gem specs and console sessions- a
:policy_storespec shared context that isolates the in-memory store per example, registered inspec/support/known_rspec_metadata_keys.yml
Part 4 of the Policy Store v2 MR split (see &22937).
The services go through the gem facade, which defaults to the in-memory repository while the persistent adapter is still in progress (#606969). They need no table, no model, and no migration to be complete and testable, and they do not change when the real adapter is configured at boot: that is what the port/adapter seam is for. Nothing calls them until the REST API (#606971), so there is no user-facing behaviour here yet.
Every service is scoped to the group organization
ListService hands organization_id to the store, which filters. FindService
and DestroyService cannot: the merged facade contract is find(id) /
delete(id), so policies are addressed by bare ID. They read the policy first
and compare organization_id in BaseService#find_policy instead.
A policy in another organization answers exactly as an ID that exists nowhere:
reason: :not_found, message Policy was not found. The response therefore
cannot be used to learn whether an ID exists somewhere else.
Pushing organization_id down into the port is still worth doing, so the filter
lives in one place rather than in each caller. Noted on
#606969.
Only one scope form is accepted
policy_scope (structured) and scope_rego (hand-authored) are alternative
authoring forms of a single scope, and the store keeps only scope_rego,
clearing policy_scope once Rego is present. A request carrying both would
therefore lose the structured scope without saying so, so CreateService
refuses it with reason: :invalid.
Deliberately left out
- The permission YAMLs listed on the issue.
bundle exec rake gitlab:permissions:validateexits 1 for both halves: assignable groups are rejected because no REST route references their raw permissions, and raw definitions are rejected because the permissions are "not found in declarative policy". They need theGroupPolicyrules and the routes that check them, so they belong with #606971. current_user/ authorization. The services take nocurrent_user; the calling endpoint is the sole enforcement point, consistent with the experiment.
References
Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/606970
Builds on !247327 (merged) (gem write API) and !247674 (merged) (scope transpiler), both now merged, so this diff is the service layer alone.
How to set up and validate locally
bin/rspec ee/spec/services/security/security_orchestration_policies/policy_store
# => 39 examples, 0 failures
cd gems/gitlab-policy-store && bundle exec rspecEach service spec runs the 'a service gated by the policy store experiment'
shared examples, so the four gate conditions and the sub-group case are covered
for all four services.
In the Rails console (the store lives in that process only, and is empty on exit):
group = Group.find_by_full_path('your-group')
ApplicationSetting.current.update!(policy_store_experiment_enabled: true)
group.namespace_settings.update!(policy_store_experiment_enabled: true)
Gitlab::CurrentSettings.expire_current_application_settings
group.policy_store_experiment_active? # => true (needs the security_policies_v2 flag + Ultimate)
create = Security::SecurityOrchestrationPolicies::PolicyStore::CreateService
result = create.new(
group: group,
params: { name: 'Console policy', trigger_id: 'deployment_requested',
actions: [{ 'type' => 'block' }],
policy_scope: { compliance_frameworks: [{ id: 5 }] } }
).execute
result.payload[:policy] # => Gitlab::PolicyStore::Policy
result.payload[:policy].scope_rego # => "package gitlab.scope\n..."
result.payload[:policy].policy_scope # => nil, the transpiled Rego replaces it
result.payload[:policy].mode # => "enforce", defaulted by the store, not the serviceBoth scope forms at once are refused:
create.new(
group: group,
params: { name: 'Two scopes', trigger_id: 'deployment_requested',
policy_scope: { compliance_frameworks: [{ id: 5 }] },
scope_rego: "package gitlab.scope\n\napplies := true\n" }
).execute.reason # => :invalidAnother organization's policy is invisible, not merely unauthorized:
# Straight to the store, so the other organization needs no experiment opt-in
elsewhere = Gitlab::PolicyStore.create(
organization_id: Organization.where.not(id: group.organization_id).first.id,
name: 'Elsewhere', trigger_id: 'deployment_requested'
)
find = Security::SecurityOrchestrationPolicies::PolicyStore::FindService
find.new(group: group, policy_id: elsewhere.id).execute.message # => "Policy was not found"
find.new(group: group, policy_id: -1).execute.message # => "Policy was not found"Security::SecurityOrchestrationPolicies::PolicyStore::ListService.new(group: group)
.execute.payload[:policies] # => only this organization's policies
Gitlab::PolicyStore.reset_configuration! # clears the scratch storeMR 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.