Fix merge request Advanced Search for Planner role

What does this MR do and why?

A Planner (access level 15) member of a private project with merge_requests_access_level set to ENABLED can open that project's merge requests: ProjectPolicy#access_allowed_to? returns true immediately for an ENABLED feature, so merge_requests_disabled is false and read_merge_request is granted.

Advanced Search (Elasticsearch) did not return any merge requests for that user, though. The membership filters take the minimum access level for merge requests from ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL[:merge_requests], which is Reporter (20), and apply that same minimum to both the ENABLED and the PRIVATE feature access levels. A Planner is below Reporter, so search filtered them out even though Ability.allowed? said they could read the merge request.

This was confirmed by probing Ability.allowed?(user, :read_merge_request, project) across every combination of project visibility, merge_requests_access_level, and role. Within those dimensions, exactly one combination diverged from what search returned: private project + ENABLED + Planner. The probe used regular users only; a separate case, an external user with the Planner role on an internal project, was found later and is covered by the same fix.

The fix adds a relaxed membership rule for that one combination. In ee/lib/search/elastic/merge_request_query_builder.rb, extra_options now sets relaxed_feature_access for merge_requests to Gitlab::Access::PLANNER, restricted to [::ProjectFeature::ENABLED]. relaxed_feature_access is an existing option in ee/lib/search/elastic/filters.rb; milestone search already uses it for the same class of mismatch (see !252980 (merged)). No change to filters.rb was needed, since this adds one extra membership rule to the query rather than widening the existing one. One shared helper, Gitlab::Elastic::SearchResults#related_ids_for_notes, does change, covered below.

The Reporter minimum still governs the PRIVATE feature access level, where ProjectPolicy genuinely denies a Planner. The change does not grant search access to anything a Planner cannot already open through the UI or API.

Merge requests matched only by a comment

Merge request search has two ways to match. One is the merge request's own title and description, handled by Search::Elastic::MergeRequestQueryBuilder. The other is a comment on the merge request: Gitlab::Elastic::SearchResults#related_ids_for_notes runs a note sub-query, collects the matching noteable_ids, and passes them to the merge request query as related_ids, which adds a should clause on those merge request ids. The result rows are still merge requests, authorized by the merge request filter; the note query is only an input supplying candidate ids. Group level and global search reach this through the same shared helper, so the project level spec covers the wiring for all three levels.

That note sub-query had the same Reporter floor, so the first fix alone was incomplete. Measured on a private project with merge_requests_access_level ENABLED, searching a term that appears only in a comment: Planner got 0 merge requests while Reporter got 1. After the fix both get 1, and a Guest still gets 0, which matches read_merge_request.

Search::Elastic::MergeRequestQueryBuilder::RELAXED_FEATURE_ACCESS is now a constant (Planner, [::ProjectFeature::ENABLED]), used by extra_options and also applied in related_ids_for_notes when the noteable type is MergeRequest. The issue-note path is deliberately untouched.

Scope boundary worth stating explicitly: this covers which merge requests appear in the Merge requests tab, including ones matched only by a comment. It does not cover comments as results in their own right. Measured for a Planner member of a project with merge_requests_access_level ENABLED, searching a term that appears only in a comment: a public project returns the comment, an internal project returns the comment, a private project returns nothing, and an internal project returns nothing for an external member, even though policy grants that member read access to the merge request. The comments gap is not limited to private projects; it appears wherever the Planner has to be authorized through project membership rather than through project visibility, which covers private projects and internal projects for external members alike. Merge request search itself does not have this gap for the external member case, because the relaxed rule applies to the public-and-internal membership clause as well as to the private clause. That remaining gap is tracked at #538209 (closed) and remains open.

doc/user/permissions.md now says: "Users with the Planner role can search merge requests. In private projects, comments on merge requests are not returned as search results for users with the Planner role." This footnote states the private project case; the external user case on internal projects is already covered by the neighbouring footnote in the same table, which says external users must be given at least the Reporter role even on internal projects. This replaces a footnote that previously said Planners could not use advanced search for merge requests at all.

Also note related_ids is skipped for global search on GitLab.com (guarded by Gitlab::Saas.feature_available?(:advanced_search)), so the comment-matched path applies to group and project scoped search on GitLab.com, and all levels on GitLab Self-Managed.

How the tests cover it

  • ee/spec/lib/search/elastic/merge_request_query_builder_spec.rb: specs for a Planner member (relaxed filter present, restricted to the ENABLED feature access level, project authorized through target_project_id), a Guest member and a non-member (no relaxed filter), a Reporter member (the Reporter minimum for the PRIVATE level is unchanged), group membership, and an external member of an internal project (the membership-free public-and-internal clause is capped at public visibility for external users, so only the relaxed membership clause can reach that project).
  • spec/support/shared_contexts/policies/project_policy_table_shared_context.rb: new permission_table_for_merge_request_access, used by the three merge request search visibility specs (global, group, project) in place of permission_table_for_reporter_feature_access. It adds the Planner rows and the :private | :enabled block that no existing table covered, which is why no spec guarded this bug. The Reporter table is unchanged and still used by the note search visibility specs, where Planner support is still pending.
  • ee/spec/lib/search/elastic/project_search_results_spec.rb: three unit examples, asserting the relaxed option is passed to Note.elastic_search for merge request notes, absent for issue notes, and that the notes scope options are otherwise left untouched so the relaxed access cannot drift into the comments results by accident, plus a three-role integration block (Planner and Reporter both get the merge request matched only by a comment, Guest gets nothing).
  • Run locally against a real Elasticsearch: 91 examples in project_search_results_spec.rb, 140 across the merge request query builder and group search results specs, 171 visibility examples, all passing. RuboCop, Vale, markdownlint and docs-link-check clean.

How to set up and validate locally

  1. Create a private project.
  2. Set the project's merge requests visibility to "Everyone with access" (merge_requests_access_level = ENABLED).
  3. Add a user as a member of the project with the Planner role.
  4. Index the project and its merge requests with Elasticsearch enabled.
  5. Sign in as that Planner user and search merge requests using Advanced Search, scoped to the project or group.
  6. Confirm the project's merge requests are now returned. Before this fix, the search returned none.
  7. Put the search term only in a comment on the merge request and confirm the merge request is still returned.

Screenshot

Before After
Screenshot_2026-09-15_at_11.34.27 Screenshot_2026-09-15_at_11.42.27

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.

Closes #538210 (closed)

Part of gitlab-org#17674

Edited by Ravi Kumar

Merge request reports

Loading
Loading