Fix Planner role in group and global merge request Basic search

What does this MR do and why?

In Basic search (not Elasticsearch), a user with the Planner role got zero merge request results at group level and at global level when the project is private and the project's merge_requests_access_level is ENABLED. Project level search was never affected.

config/authz/roles/planner.yml grants the Planner role read_merge_request. In ProjectPolicy, access_allowed_to? returns true immediately for a feature at ENABLED, so merge_requests_disabled is false and read_merge_request is granted. The policy says a Planner should see these merge requests.

The bug was in IssuableFinder::Params#projects, which chains two filters: projects_public_or_visible_to_user, backed by Project.public_or_visible_to_user(current_user, min_access_level), and .with_feature_available_for_user(MergeRequest, current_user). min_access_level came from ProjectFeature.required_minimum_access_level(MergeRequest), which is Reporter (20). Reporter is the correct floor for the PRIVATE feature access level, and the second filter already enforces it correctly, applying the Reporter check only inside the = PRIVATE branch, mirroring Featurable#has_permission?. Applying that same Reporter value a second time as a whole-project visibility floor excluded Planner (15) before the feature filter ever got a chance to run.

Project level search always worked because Gitlab::ProjectSearchResults#issuable_params merges in project_id. That makes IssuableFinder::Params#project? true, which short-circuits #projects on its first line to Array.wrap(project). IssuableFinder::Params#project authorizes with a direct Ability.allowed?(current_user, :read_merge_request, project) call, so min_access_level is never consulted on that path. Group and global search have no single project to check, so they fall through to the SQL filters and hit the bug.

The fix is one override in app/finders/merge_requests_finder/params.rb: min_access_level now returns Gitlab::Access::PLANNER instead of inheriting the Reporter value. The Reporter floor still governs merge_requests_access_level: PRIVATE, enforced by with_feature_available_for_user, which is unchanged.

To verify, a probe compared Ability.allowed?(user, :read_merge_request, merge_request) against what the finder returned, across all 18 combinations of project visibility (public, internal, private), merge_requests_access_level (enabled, private), and role (guest, planner, reporter). Before the fix exactly one cell diverged: private project, ENABLED, Planner. After the fix all 18 cells match. Guest is still excluded in every cell where policy denies.

A second case surfaced separately: an external user holding the Planner role on an internal project. External users only see PUBLIC by visibility level, so they can only reach an internal project through the membership clause, which the Reporter floor was also blocking. The same one line fixes it.

The change is in the finder, not in search code. MergeRequestsFinder is also used without a project_id by the group and dashboard merge request lists and by REST and GraphQL merge request endpoints. Those had the same gap for Planners in private projects and are fixed by the same change.

New tests in three spec files. All but the last bullet's guard fail on master (verified by reverting only the app change and re-running):

  • spec/finders/merge_requests_finder_spec.rb: a Planner in a private project now sees its merge requests; an external user with the Planner role on an internal project now sees its merge requests.
  • spec/services/search/group_service_spec.rb: in a private project, a Planner gets the merge request and a Guest gets nothing.
  • spec/services/search/global_service_spec.rb: the same pair at global level.
  • spec/finders/merge_requests_finder_spec.rb: in a private project with merge_requests_access_level: PRIVATE, a Planner is still excluded and a Reporter is included. This guard passes on master; it fails if with_feature_available_for_user stops enforcing the Reporter floor.

No feature flag. This aligns search with an ability the role already has, matching the precedent set by the Advanced Search equivalent.

No documentation change. The Planner footnote in doc/user/permissions.md covers advanced search for comments only. It never claimed a Basic search limitation.

References

  • Part of epic "Support Planner role in Basic search": gitlab-org#21063
  • The Advanced Search equivalent, which fixed the same single divergent cell one layer down using relaxed_feature_access in the Elasticsearch filters: !253160 (merged)
  • The epic description's scope table currently marks Merge requests as supported for Basic search, but that only held at project level.

Screenshots or screen recordings

Search level Before After
Global Screenshot_2026-09-23_at_12.42.24 Screenshot_2026-09-23_at_12.47.40
Group Screenshot_2026-09-23_at_12.42.40 Screenshot_2026-09-23_at_12.47.54
Project Screenshot_2026-09-23_at_12.42.57 Screenshot_2026-09-23_at_12.48.05

How to set up and validate locally

  1. Create a group with a private project in it.
  2. Leave the project's merge requests visibility at "Everyone with access" (merge_requests_access_level: ENABLED).
  3. Create a merge request in it with a distinctive word in the title.
  4. Add a user to the project with the Planner role.
  5. Sign in as that user, go to the group's search page, search that word, and open the Merge requests tab. Before this change the tab is empty. After it the merge request is listed.
  6. Repeat from the global search page for the same result.
  7. Confirm a Guest on the same project still sees nothing.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Ravi Kumar

Merge request reports

Loading
Loading