Draft: Serve the Policy Store to groups through group abilities

What does this MR do and why?

The group Policy Store pages authorize through the group-level *_govern_policy abilities (which group Owners hold via the authz role definitions), but the data they render came from the organization-scoped REST endpoints, which only organization owners and instance administrators may call. On SaaS, group Owners are never owners of the default organization, so the page rendered and every list/create call returned 403 — the "Policy store is broken on staging" report.

This MR gives the group surface its own authorized data path, end to end:

  • Group REST endpoints (/groups/:id/security/policy_store, full CRUD) authorize the existing group abilities (boundary_type: :group; the govern_policy granular token permissions gain the group boundary). The payload params are shared with the organization endpoints via Grape shared params, so the two surfaces cannot drift.
  • Services take a container — a Group or an Organization — as the authorization subject. A group container stamps created policies with the group's namespace (govern_policies.namespace_id, which existed for this), lists only that namespace, and reports policies outside it as not found, indistinguishably from a missing id. The organization surface keeps managing every policy of the organization, including group-stamped ones.
  • The store port's list gains a namespace_id filter (gem gitlab-policy-store).
  • Evaluation is scoped: Govern::Policy.evaluation_candidates now takes namespace_ids and returns organization-wide policies plus only the named namespaces' policies. With none named, only organization-wide policies evaluate — a group-authored policy can never enforce outside its subtree by default. (No in-repo caller passes namespaces yet; the default is the safe one.)
  • The frontend picks its surface from an injected scope ({ groupId } on group pages, { organizationId } on organization pages), so the group pages call the endpoints their controllers' abilities actually match.

References

Follow-up to the staging triage that produced !252552 (closed) and !252544 (closed). No tracked issue yet.

Screenshots or screen recordings

No visual change: the group list/wizard pages render as before, but their data calls now succeed for group Owners. Before, a group Owner saw the page with the alert "You do not have permission to view the policies of this organization." and an empty table; after, the list loads and the wizard saves.

How to set up and validate locally

  1. Enable the experiment and set up a group Owner who does not own the organization:

    Feature.enable(:security_policies_v2)
    ApplicationSetting.current.update!(policy_store_experiment_enabled: true)
    group = Group.find_by_full_path('<your-root-group>')
    group.namespace_settings.update!(policy_store_experiment_enabled: true)
    user = User.find_by_username('<some-user>')
    group.add_owner(user)
  2. As that user, open Secure > Policy store on the group: the list loads without the permission alert (previously every data call returned 403).

  3. Create a policy through the wizard: the save succeeds, and POST /api/v4/groups/:id/security/policy_store returns 201 with "namespace_id" set to the group's id.

  4. Confirm the boundaries hold with the user's PAT:

    # The group surface serves only the group's own policies:
    curl --header "PRIVATE-TOKEN: <user-token>" "http://gdk.test:3000/api/v4/groups/<group-id>/security/policy_store"
    # The organization surface still refuses a non-owner of the organization:
    curl --header "PRIVATE-TOKEN: <user-token>" "http://gdk.test:3000/api/v4/organizations/1/security/policy_store"

    The first returns 200 with the group's policies; the second stays 403.

  5. As an organization owner or admin, GET /api/v4/organizations/1/security/policy_store lists both organization-wide and group-stamped policies.

Merge request reports

Loading
Loading