Add the policy store list endpoint
What does this MR do and why?
Adds GET /organizations/:id/security/policy_store, the first Policy Store route that
returns policy records rather than a static catalogue. It resolves the organization with
find_organization!, checks read_govern_policy on it, and presents ListService's
payload through the new API::Entities::Govern::Policy.
The organization tenancy this route needs was split out into !249226 (merged), which has merged. There are no service changes here, and the store gem is untouched.
The policy entity carries the gem's current field set, after
the schema alignment
landed on master: trigger_type rather than trigger_id, plus namespace_id,
description, created_at, and updated_at.
Design decisions
read_govern_policy is enabled for organization owners and instance admins, on
EE::Organizations::OrganizationPolicy, matching the cd_application and
create_duo_flow_callback_hook blocks already in that file. Not
access_organization_admin_area, which means "can open the organization admin area" and
is gated behind the org_admin_area release, so reusing it would tie policy authoring to
an unrelated rollout.
The granular token boundary is instance. Authz::Validation::BOUNDARIES is
%w[instance group project user], so there is no organization boundary to declare. Every
organization-scoped route in lib/api/organizations.rb does the same, including
DELETE /organizations/:id. The consequence is that a granular token scoped to a group
cannot call this endpoint. Two examples in the granular token shared example report as
pending for the same reason: they only assert on project and group boundaries.
Unlike the catalogue routes, this one authenticates. A caller who is not an
organization owner gets 403 for a public organization and 404 for a private one, since
find_organization! resolves nothing it cannot read.
The experiment gate is checked twice, deliberately. The shared before block checks
the feature flag, the instance setting, and the licence, and
Organizations::Organization#policy_store_experiment_active? checks the same three, so
ListService cannot report the experiment inactive to a request that got past the gate.
The service keeps gating itself for callers that do not come through the API, which is why
the request spec stubs ListService to cover that branch.
doc/api/openapi/openapi_v3.yaml does not change. The route is hidden true, so it
contributes only the policy_store tag, and that tag already exists.
How to set up and validate locally
Requires an Ultimate licence.
Gitlab::PolicyStore is backed by an in-memory repository built lazily per process, and
nothing configures it at boot, so a policy created on the rails console is invisible to the
process serving the request. Seeding therefore happens in an initializer, which runs in
every process including Puma. The ActiveRecord-backed repository in
https://gitlab.com/gitlab-org/gitlab/-/work_items/606969 removes the need for this.
- Enable the
security_policies_v2feature flag on the rails console
Feature.enable(:security_policies_v2)- As an administrator, go to Admin > Settings > Security and compliance and turn on the policy store experiment
- On the rails console, confirm the gate is satisfied, pick an organization you own, and note your GDK's base URL
organization = Organizations::Organization.find(1)
organization.policy_store_experiment_active? # => true
puts Gitlab.config.gitlab.url # for example https://gdk.test:3443- Make yourself an owner of that organization, if you are not already
user = User.find_by_username('root')
Organizations::OrganizationUser.find_or_create_by!(organization: organization, user: user) do |organization_user|
organization_user.access_level = Gitlab::Access::OWNER
end- Seed a policy into the process that serves the request, by adding
config/initializers/zz_temporary_policy_store_seed.rb, then runninggdk restart rails-web. Delete the file when you are done
# frozen_string_literal: true
Rails.application.config.after_initialize do
next unless Rails.env.development?
Gitlab::PolicyStore.create(
organization_id: 1,
name: 'Block deployments on critical findings',
trigger_type: 'deployment_requested'
)
end- Create a personal access token for yourself: select your avatar in the upper-right corner, select Edit profile, then in the left sidebar select Access > Personal access tokens
- From the Generate token dropdown list select Legacy token, enter a Token name, leave Expiration date empty, select the
apiscope, then select Generate token. Copy the token, since it is shown only once - List the policies with that token, against the base URL from step 3
curl --header "PRIVATE-TOKEN: <your token>" \
--url "<your GDK URL>/api/v4/organizations/1/security/policy_store"- Verify the response carries the seeded policy, with
organization_idmatching the organization from step 3
[
{
"id": 1,
"organization_id": 1,
"namespace_id": null,
"name": "Block deployments on critical findings",
"description": null,
"version": 1,
"trigger_type": "deployment_requested",
"rules": [],
"actions": [],
"policy_scope": null,
"scope_rego": "package gitlab.scope\n\nimport rego.v1\n...",
"mode": "enforce",
"lifecycle_state": "active",
"created_at": null,
"updated_at": null
}
]namespace_id is null because the policy is owned by the organization rather than by a
group, and scope_rego holds the applies-to-all program the store compiles when a policy
has no scope. created_at and updated_at stay null until the ActiveRecord-backed
repository replaces the in-memory one.
References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/606971
- Part of https://gitlab.com/groups/gitlab-org/-/epics/22937
- Depends on !249226 (merged) (merged), which moved the authoring services to organization tenancy
- The gem change that lets a policy be owned by an organization alone: !249164 (merged) (merged)
- The ActiveRecord-backed repository, which replaces the in-memory store: https://gitlab.com/gitlab-org/gitlab/-/work_items/606969
- Pagination and the list-versus-show entity split: #608267 (closed)