Fix commit Advanced Search for Planner role in private projects

What does this MR do and why?

A user with the Planner role who is a member of a private project got no results from Advanced Search (Elasticsearch) when searching commits in that project.

Root cause

ProjectPolicy grants a Planner read_code: read_code is listed in config/authz/roles/planner.yml, and ProjectPolicy#access_allowed_to?(:repository) only requires team_access_level >= ProjectFeature.required_minimum_access_level(:repository), which is Guest.

The Elasticsearch membership filters in ee/lib/search/elastic/filters.rb were stricter. They take the minimum access level for the repository feature from ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT[:repository], which is Reporter, and apply it to private projects. A Planner passed the policy check but was filtered out of search.

Probing Ability.allowed?(user, :read_code, project) across all 45 combinations of project visibility, repository access level and role pins where the two disagree. Planner is true in a private project at both the enabled and the private repository access level, and false when the repository is disabled. Guest is false in every private project combination. Public and internal projects already matched, because the minimum there is Guest for both.

So the divergence is the whole private-project column for the Planner role, at both feature access levels. That is why the relaxed rule below covers [ENABLED, PRIVATE] rather than just ENABLED as the merge request fix does.

Fix

Elastic::Latest::GitClassProxy::COMMIT_RELAXED_FEATURE_ACCESS sets relaxed_feature_access for the repository feature to a Planner minimum over the ENABLED and PRIVATE project feature access levels. It is merged into the options only in the 'commit' branch of GitClassProxy#elastic_search, which is the single funnel for commit search: find_commits_by_message_with_elastic on both the class and the instance proxy routes through it, so global, group and project level search are all covered by one place.

relaxed_feature_access is an existing option in ee/lib/search/elastic/filters.rb. It adds a second, lower membership rule rather than widening the existing one. Milestone search introduced it in !252980 (merged), and merge request search uses it in !253160 (merged).

Two shared changes in ee/lib/search/elastic/filters.rb. First, a relaxed rule only applies to the branch where it lowers the minimum access level. For the repository feature it stays on the private branch (Reporter down to Planner) and is dropped from the public/internal branch, where the minimum is already Guest. Each branch also looks up ids only for the access levels its own rules use, so commit search does 3 project_authorizations lookups: Guest on public/internal, Reporter and Planner on private.

Second, build_feature_access_level_filter removes duplicate predicates. On the private branch, for a Reporter or above, the base and relaxed rules resolve to the same project ids and emit the same repository_access_level predicate, so they collapse into one clause. Together, these keep the query body unchanged for every role other than Planner.

Scope: commit search only. Blob (code) search and Zoekt exact code search still use the Reporter minimum for private projects, and comments on commits are unchanged. Epic gitlab-org#17674 stays open for those.

Why not lower the shared constant?

An earlier version of this MR lowered PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT[:repository] from Reporter to Planner. That constant is also read by Project.filter_by_feature_visibility and by Search::GroupsFinder, so the change reached project visibility filtering outside search. It would also have enabled blob search and Zoekt exact code search for Planners in private projects, which doc/user/permissions.md still lists as unsupported for that role. The spec diff of that version showed it: it had to add a Planner row expecting results to the private-project blob table, while the footnote it edited still said code search does not work for Planners.

Tests

  • spec/support/shared_contexts/policies/project_policy_table_shared_context.rb gains permission_table_for_repository_access, used by the global, group and project commit visibility specs in place of permission_table_for_guest_feature_access_and_non_private_project_only. It adds the Planner rows and a :private | :enabled block that no existing table covered, which is why no spec guarded this bug. The old table is unchanged and still used by the blob visibility specs, where Planner support is still pending.
  • ee/spec/lib/search/elastic/filters_spec.rb asserts that with the relaxed option a Planner member of a private project produces a query byte-identical to the existing global_search_user_access_to_project_as_reporter.json fixture, that a Reporter's query is unchanged, and that a Guest's query is unchanged. Removing terms.uniq! fails the Reporter example, so the dedupe is covered. A context with a Guest in one public project and a Reporter in another asserts the query is identical with and without relaxed_feature_access, so the relaxed rule has no effect on public and internal projects.
  • ee/spec/lib/elastic/latest/git_class_proxy_spec.rb gains a Planner row expecting no results for blob search in a private project. This pins the boundary that code search still requires Reporter. The blob visibility specs can't show it because their table has no Planner rows.

References

How to set up and validate locally

  1. Enable Elasticsearch and Advanced Search in the GDK.
  2. Create a private project with a repository and at least one commit.
  3. Leave the project's repository visibility at Everyone with access, or set it to Only project members. Both are covered by the fix.
  4. Add a user to the project with the Planner role.
  5. Index the project so its commits reach Elasticsearch.
  6. Sign in as that user and search commits, scoped to the project, the group, and globally.
  7. Confirm the commit is returned in all three. Before this change, all three returned nothing.
  8. Repeat as a Guest member and confirm no commits are returned, which matches read_code.
  9. Repeat a code search as the Planner and confirm it still returns nothing, which confirms the change did not widen code search.

Screenshot

Before After
Screenshot_2026-09-17_at_12.33.01 Screenshot_2026-09-17_at_12.13.12

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