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.

  1. Enable the security_policies_v2 feature flag on the rails console
Feature.enable(:security_policies_v2)
  1. 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.
  2. 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
  1. 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
  1. Seed one policy per organization into the process that serves requests, by adding config/initializers/zz_temporary_policy_store_seed.rb and running gdk restart rails-web (delete the file when done). The in-memory adapter numbers ids from one per process, so the first policy is id 1 and the second is id 2
# 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
  1. Create a personal access token with the api scope for root, since the second organization is created private with no members, so only an administrator can read it.
  2. List the first organization's policies to confirm the seed reached the process that serves requests, then fetch policy 1 and 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.

  1. Request the second organization's policy (id 2) through the first organization's path. It returns 404 {"message":"404 Policy Not Found"}, because BaseService#find_policy drops a policy whose organization_id does not match the one in the path (the same initializer seeded policy 2, so this cannot be a missing seed). Requesting an id no policy has, for example 999999, 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

Edited by Marcos Rocha

Merge request reports

Loading
Loading