Add the policy store show endpoint
What does this MR do and why?
Adds GET /organizations/:id/security/policy_store/:policy_id, which returns a single policy from the store.
A policy owned by another organization returns the same 404 as a missing one, both {"message":"404 Policy Not Found"}, so a policy id cannot be probed across organizations. That is enforced in Ruby by BaseService#find_policy rather than in the store, because the facade's find(id) takes no organization_id and a policy id is global. It is the only guard until the ActiveRecord-backed repository lands.
Resolving and authorizing the organization is now shared by both routes as authorized_organization!(ability). The ability is a parameter because the create, delete and update routes each need their own: !249155 (merged), !249158 (merged), and !249174 (merged).
The gate coverage moved into an an organization-scoped policy store endpoint shared example covering eight cases, the same way the catalogue routes did in !248872 (merged). The outer block is now describe 'the organization-scoped routes'. The error-mapping contexts stayed under the list route, because both routes share render_policy_store_error!.
The show route now asserts the response schema the way the list route already does. doc/api/openapi/openapi_v3.yaml does not change since the route is hidden true, and doc/auth/tokens/fine_grained_access_tokens_rest.md gains one row for the new route, regenerated with bundle exec rake gitlab:permissions:routes:compile_docs, which is why this MR carries the documentation label.
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.
- 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, expand Security policies, select the Allow Policy Store experiment for groups checkbox, then select Save changes.
- On the rails console, confirm both gates the route checks, and note the base URL
organization = Organizations::Organization.find(Organizations::Organization::DEFAULT_ORGANIZATION_ID)
user = User.find_by_username('root')
organization.policy_store_experiment_active? # => true
Ability.allowed?(user, :read_govern_policy, organization) # => true
puts Gitlab.config.gitlab.url # for example https://gdk.test:3443- Create the second organization the cross-organization check needs, and print its id
other_organization = Organizations::Organization.find_by(path: 'other-org') ||
Organizations::Organization.create!(name: 'Other', path: 'other-org')
puts other_organization.id- Seed one policy per organization into the process that serves requests, by adding
config/initializers/zz_temporary_policy_store_seed.rband runninggdk restart rails-web(delete the file when done). The in-memory adapter numbers ids from one per process, so the first policy is id1and the second is id2
# frozen_string_literal: true
Rails.application.config.after_initialize do
next unless Rails.env.development?
Gitlab::PolicyStore.create(
organization_id: Organizations::Organization::DEFAULT_ORGANIZATION_ID,
name: 'Block deployments on critical findings',
trigger_type: 'deployment_requested'
)
other_organization = Organizations::Organization.find_by(path: 'other-org')
next unless other_organization
Gitlab::PolicyStore.create(
organization_id: other_organization.id,
name: 'Other organization policy',
trigger_type: 'deployment_requested'
)
end- Create a personal access token with the
apiscope forroot, since the second organization is created private with no members, so only an administrator can read it. - List the first organization's policies to confirm the seed reached the process that serves requests, then fetch policy
1and verify the response is a single JSON object rather than an array
curl --header "PRIVATE-TOKEN: <your token>" \
--url "<your GDK URL>/api/v4/organizations/1/security/policy_store"
curl --header "PRIVATE-TOKEN: <your token>" \
--url "<your GDK URL>/api/v4/organizations/1/security/policy_store/1"{
"id": 1,
"organization_id": 1,
"name": "Block deployments on critical findings",
"rules": [],
"mode": "enforce",
"lifecycle_state": "active"
}Abbreviated above. The full body carries all fifteen fields of
API::Entities::Govern::Policy, with policy_scope, created_at and updated_at null.
- Request the second organization's policy (id
2) through the first organization's path. It returns404 {"message":"404 Policy Not Found"}, becauseBaseService#find_policydrops a policy whoseorganization_iddoes not match the one in the path (the same initializer seeded policy2, so this cannot be a missing seed). Requesting an id no policy has, for example999999, returns a byte-identical response, so a caller cannot tell the two apart
curl --header "PRIVATE-TOKEN: <your token>" \
--url "<your GDK URL>/api/v4/organizations/1/security/policy_store/2"References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/606971
- Part of https://gitlab.com/groups/gitlab-org/-/epics/22937
- Depends on !249148 (merged) (merged), which added the list endpoint.
- Depends on !249226 (merged) (merged), which moved the policy store services to organization tenancy.
- The ActiveRecord-backed repository, which replaces the in-memory store: https://gitlab.com/gitlab-org/gitlab/-/work_items/606969