Add the policy store delete endpoint
What does this MR do and why?
Adds DELETE /organizations/:id/security/policy_store/:policy_id to API::Govern::Policies, gated by delete_govern_policy for organization owners and instance admins.
DestroyService, the organization lookup and the error mapping already exist, so the rest is the route, its permission and its specs. The route reuses the authorized_organization! and render_policy_store_error! helpers the read routes use, and declares the same failure list, including the 500 that helper can return when a service reason has no mapping.
The permission is delete-specific rather than one write ability covering create, update and delete, matching !249442 (merged). A granular token can then be scoped to deleting policies without also being able to create them.
A policy owned by another organization returns the same 404 Policy Not Found as a missing one and stays in the store, so an id cannot be used to delete across organizations. That comes from BaseService#find_policy, which the show endpoint relies on for the same guarantee.
boundary_type: :instance is wider than the resource, and is a workaround rather than a choice: Authz::Boundary supports project, group, user and instance only, so an organization-scoped route has nowhere else to sit. Per-organization enforcement still happens in authorized_organization!, which authorizes delete_govern_policy against the organization the id names. The read routes already carry the same boundary for the same reason.
How to set up and validate locally
The store is in-memory and per process. The rails console holds its own copy that the web
server never sees, so seeding with Gitlab::PolicyStore.create on the console and then deleting
over HTTP always answers 404. The seed therefore goes in an initializer, which runs inside the
Puma process, and every check below goes through the API.
Requires an Ultimate licence and an organization owner. Run GDK with a single Puma worker, since without preloading each worker seeds and holds its own copy:
gdk config set gitlab.rails.puma.workers 1
gdk reconfigure- Enable the experiment on the rails console, confirm the gate is open, and note the base URL
Feature.enable(:security_policies_v2)
ApplicationSetting.current.update!(policy_store_experiment_enabled: true)
organization = Organizations::Organization.find(Organizations::Organization::DEFAULT_ORGANIZATION_ID)
organization.policy_store_experiment_active? # => true
puts "organization #{organization.id}, url #{Gitlab.config.gitlab.url}"That one call ANDs the feature flag, the instance setting and the licence. If it returns false,
every step below answers 403 or 404 without saying which gate closed.
- Seed a policy into the web process, in
config/initializers/zz_temporary_policy_store_seed.rb
# 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'
)
end- Restart the web process so the initializer runs
gdk restart rails-web- List the policies with a personal access token that has the
apiscope, and note theid. This step is also what proves the web process can see the seed, which the console cannot tell you
curl --header "PRIVATE-TOKEN: <your_token>" \
"<your GDK URL>/api/v4/organizations/<organization_id>/security/policy_store"- Delete it, substituting that id
curl --request DELETE --header "PRIVATE-TOKEN: <your_token>" --write-out '\nHTTP %{http_code}\n' \
"<your GDK URL>/api/v4/organizations/<organization_id>/security/policy_store/<policy_id>"- Verify the response is
HTTP 204with an empty body. The--write-outis what makes the status visible, since a 204 prints nothing on its own. - Repeat step 5 and verify it now returns
HTTP 404with{"message":"404 Policy Not Found"}, confirming the policy is gone rather than only hidden. - Confirm the permission gates the route. Give an organization member who is not an owner a token, on the rails console. Tokens live in the database, so the console is fine here
member = User.find_by_username('<a username that is not an organization owner>')
membership = Organizations::OrganizationUser.find_or_initialize_by(organization: organization, user: member)
membership.update!(access_level: :default)
puts member.personal_access_tokens.create!(name: 'policy store member check', scopes: [:api], expires_at: 30.days.from_now).token- Restart the web process again to restore the seeded policy, then repeat step 5 with the
member's token and verify
HTTP 403. Repeat step 4 as the owner to confirm the policy is still listed, so the refusal left it alone. - Remove
config/initializers/zz_temporary_policy_store_seed.rband put the worker count back
gdk config set gitlab.rails.puma.workers 2
gdk reconfigure
gdk restart rails-webReferences
- Part of https://gitlab.com/gitlab-org/gitlab/-/work_items/606971
- Related to !249155 (merged) (open), the create endpoint. The two are independent and can be reviewed in parallel: whichever merges second needs a mechanical rebase, since both append to
ee/lib/api/govern/policies.rb, the organization policy and the two specs. - Uses the same per-action permission split as !249442 (merged) (open)