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 matchesBlobPolicy.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): forfeatures: [:repository], groups where the user is a Planner are now included. No production code passesfeatures: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 throughrelaxed_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.rbexpects Planner for the repository.permission_table_for_guest_feature_access_and_non_private_project_onlygets Planner rows. A Planner sees a private repository in a private project. This table is used byspec/models/project_spec.rb,spec/policies/blob_policy_spec.rb, and the Zoekt blob visibility specs.- The
Search::GroupsFindercache-key specs now derive the level fromProjectFeatureinstead 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
- Closes #632187 (closed)
- Follow-up from !259653 (merged)
- Related issue: #538212 (closed)
- Epic: gitlab-org#17674 (closed)
Screenshots or screen recordings
Not applicable. This change has no UI.
How to set up and validate locally
-
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.
-
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_projectsin a Rails console. -
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 returnedfalse.
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.