Let Planners read private repositories in filter_by_feature_visibility

What does this MR do and why?

The Planner role grants read_code (see config/authz/roles/planner.yml). Because of this, BlobPolicy lets a Planner read code in a private project whose repository feature is private.

Project.filter_by_feature_visibility(:repository, user) disagreed. It required Reporter for that case, through ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: REPORTER }. So the scope hid projects that the policy allows.

This MR sets that floor to Gitlab::Access::PLANNER.

Access levels are Guest 10, Planner 15, Reporter 20, Security Manager 25. Planner is the only role between 15 and 19. So only Planners gain access. Custom roles based on Planner also gain access, and they already get read_code from the role. No other role changes.

This is a consistency fix, not a user-facing bug fix. No current caller depends on the private-project repository floor for a Planner, so no search results or pages change today. The scope now gives the same answer as the policy for any future caller.

Consumers

These call ProjectFeature.required_minimum_access_level_for_private_project:

  • Project.filter_by_feature_visibility (app/models/project.rb): now returns private-repository projects to a Planner member. This matches BlobPolicy.
  • Project.filter_out_public_projects_with_unauthorized_private_repos (used by Zoekt search results): only looks at public projects, where the private-project floor doesn't apply. No change.
  • Search::GroupsFinder#min_access_level (ee/app/finders/search/groups_finder.rb): for features: [:repository], groups where the user is a Planner are now included. No production code passes features: today, so this only shows in specs.
  • Advanced Search authorization (ee/lib/search/elastic/concerns/authorization_utils.rb): the private-project level for repository drops to Planner. Advanced Search already let Planners see code and commits through relaxed_feature_access, so results do not change. That relaxed rule may now be redundant for the repository feature. This MR leaves it in place.

Spec changes:

  • spec/models/project_feature_spec.rb expects Planner for the repository.
  • permission_table_for_guest_feature_access_and_non_private_project_only gets Planner rows. A Planner sees a private repository in a private project. This table is used by spec/models/project_spec.rb, spec/policies/blob_policy_spec.rb, and the Zoekt blob visibility specs.
  • The Search::GroupsFinder cache-key specs now derive the level from ProjectFeature instead of hardcoding 20.

This MR edits a different table in the same shared spec file as !259653 (merged). The two MRs do not conflict.

Why this has no user-facing effect today

ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT sets repository: REPORTER. That value predates the Planner role (access level 15). It exists so Guests can't read code in private projects. Every user-facing path already lets a Planner read private-repository code, so changing the constant has no visible effect.

When the Planner role was added with read_code, each search engine that hit the mismatch added its own Planner exception. None of them changed the shared constant. The constant is only reached when Project.filter_by_feature_visibility(:repository) is called directly for a private project, and no user-facing path does that.

Consumer Uses the Reporter floor? Why a Planner still gets code
Zoekt code search, Search::Zoekt::AccessBranchBuilder (through Search::AuthorizationContext#get_access_levels_for_feature) Yes, access_levels[:private_project] For features == 'repository', private_min_access_level returns [access_levels[:private_project], PRIVATE_RELAXED_MIN_ACCESS_LEVEL].min. PRIVATE_RELAXED_MIN_ACCESS_LEVEL is ::Gitlab::Access::PLANNER.
Advanced Search code and commit search, ee/lib/search/elastic/concerns/authorization_utils.rb Yes, private_project: Elastic::Latest::GitClassProxy passes REPOSITORY_RELAXED_FEATURE_ACCESS, a second relaxed_feature_access rule that covers Planner.
Zoekt result filtering, Project.filter_out_public_projects_with_unauthorized_private_repos Calls filter_by_feature_visibility(:repository) It only applies to public projects, where the private-project floor doesn't apply.
Search::GroupsFinder with features: Yes No production caller passes features:. Callers pass an explicit min_access_level.

Basic (Postgres) code search and repository pages check Ability (read_code, download_code). The Planner role grants both, so these paths never used this floor.

Once the floor is Planner, Zoekt's PRIVATE_RELAXED_MIN_ACCESS_LEVEL clamp and Advanced Search's repository relaxed_feature_access rule are redundant. A later MR can remove them to leave one source of truth, and that work is not part of this change.

References

Screenshots or screen recordings

Not applicable. This change has no UI.

How to set up and validate locally

  1. Pick a private project. In Settings > General > Visibility, project features, permissions, set Repository, Merge requests, and CI/CD to Only Project Members. The repository can't be restricted while merge requests or CI/CD are more visible.

  2. Add a user who is not otherwise a member as a Planner. Project authorizations refresh in Sidekiq, so wait a moment, or run user.refresh_authorized_projects in a Rails console.

  3. In a Rails console, compare these two results:

    Project.where(id: project.id).filter_by_feature_visibility(:repository, user).exists?
    Ability.allowed?(user, :read_code, project)

    Both return true. Before this change, the first returned false.

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.

🤖 Generated with Claude Code

Edited by Ravi Kumar

Merge request reports

Loading
Loading