Draft: Show group container registry to non-members and anonymous users of public groups

What does this MR do and why?

On a public group, the Deploy > Container Registry page was only available to group members with at least the Reporter role. Everyone else, including visitors who are not signed in, did not see the sidebar entry and got a 404 at /groups/<group>/-/container_registries, even when the group contains public projects with public container images. Projects already work the way you would expect: a public project's container registry is visible to everyone.

With this change, anyone who can view a public group can open its container registry page and see the images from the projects in that group that they are allowed to see:

  • Public projects with a public registry: shown to everyone, including users who are not signed in.
  • Public projects whose registry visibility is "Only Project Members": hidden from anonymous and non-member users. Shown only to project members with at least the Reporter role.
  • Private projects: never shown to users who cannot access them.
  • Private groups: unchanged. Non-members and anonymous users still get a 404.
  • Internal groups: signed-in users gain the same access, because the public_authenticated role inherits from public_anonymous.

The same permission and finder are used by the REST API (GET /groups/:id/registry/repositories) and the GraphQL field Group.containerRepositories, so they gain the same behavior.

Technical details

  • config/authz/roles/public_anonymous.yml: adds read_container_image to the group raw permissions. GroupPolicy grants these on public groups (rule { public_group }), and through public_authenticated inheritance to signed-in users on internal groups. This mirrors the existing project entry in the same role.
  • app/models/container_repository.rb: adds a new scope for_group_and_its_subgroups_visible_to_user(group, user). It is the same group-scoped INNER JOIN subquery as the existing for_group_and_its_subgroups, but uses Project.filter_by_feature_visibility(:container_registry, user) instead of with_feature_enabled(:container_registry). That helper combines with_feature_available_for_user (respects the "Only Project Members" registry setting, Reporter minimum) and public_or_visible_to_user (respects project visibility). The existing for_group_and_its_subgroups scope is unchanged because Group#has_container_repository_including_subgroups? uses it and must see all repositories regardless of user.
  • app/finders/container_repositories_finder.rb: group_repositories now uses the new scope. Before this change the finder returned every repository in the group hierarchy once the read_container_image check passed, which was safe only because that check required Reporter membership.
  • doc/user/packages/container_registry/_index.md: documents the group registry visibility.
  • Specs: finder, model, group policy (and shared context), request spec for the group registry controller, and sidebar menu.

Database

Only ContainerRepositoriesFinder#group_repositories changes. The query keeps the same shape as on master (a single INNER JOIN on a group-scoped projects subquery); the subquery gains the registry access level and project visibility conditions.

BEFORE (master, ContainerRepository.for_group_and_its_subgroups(group)):

SELECT "container_repositories".*
FROM "container_repositories"
INNER JOIN (
  SELECT "projects"."id"
  FROM "projects"
  INNER JOIN "project_features" ON "project_features"."project_id" = "projects"."id"
  WHERE "projects"."namespace_id" IN (<group and descendants via traversal_ids CTE>)
    AND ("project_features"."container_registry_access_level" IS NULL
         OR "project_features"."container_registry_access_level" > 0)
) projects ON projects.id = container_repositories.project_id

AFTER, anonymous user (user: nil):

SELECT "container_repositories".*
FROM "container_repositories"
INNER JOIN (
  SELECT "projects"."id"
  FROM "projects"
  INNER JOIN "project_features" ON "project_features"."project_id" = "projects"."id"
  WHERE "projects"."namespace_id" IN (<group and descendants via traversal_ids CTE>)
    AND ("project_features"."container_registry_access_level" IN (20, 30)
         OR "project_features"."container_registry_access_level" IS NULL)
    AND "projects"."visibility_level" = 20
) projects ON projects.id = container_repositories.project_id

AFTER, signed-in user (:user_id; 20 is the Reporter access level):

SELECT "container_repositories".*
FROM "container_repositories"
INNER JOIN (
  SELECT "projects"."id"
  FROM "projects"
  INNER JOIN "project_features" ON "project_features"."project_id" = "projects"."id"
  WHERE "projects"."namespace_id" IN (<group and descendants via traversal_ids CTE>)
    AND ("project_features"."container_registry_access_level" IS NULL
         OR "project_features"."container_registry_access_level" IN (20, 30)
         OR ("project_features"."container_registry_access_level" = 10
             AND EXISTS (SELECT 1 FROM "project_authorizations"
                         WHERE "project_authorizations"."user_id" = :user_id
                           AND "project_authorizations"."access_level" >= 20
                           AND "project_authorizations"."project_id" = "project_features"."project_id")))
    AND (EXISTS (SELECT 1 FROM "project_authorizations"
                 WHERE "project_authorizations"."user_id" = :user_id
                   AND "project_authorizations"."access_level" >= 20
                   AND "project_authorizations"."project_id" = "projects"."id")
         OR "projects"."visibility_level" IN (10, 20))
) projects ON projects.id = container_repositories.project_id

Note for the database reviewer: the SQL above is derived from the ActiveRecord scope chain; the author does not have access to postgres.ai or Database Lab. Please generate the actual SQL and query plans for a large public group such as gitlab-org (id 9970) for the anonymous, signed-in non-member, and group member cases, and confirm the structure matches.

References

Closes #390311

Earlier attempt: !239908 (closed) (closed)

Screenshots or screen recordings

Screenshots to be added before marking ready.

Before After
Signed-out visitor on a public group: no Container Registry entry under Deploy, and the direct URL returns 404 Entry is shown and the page lists images from the group's public projects

How to set up and validate locally

  1. Enable the container registry in your GDK.
  2. Create a public group containing:
    • a public project with a container image pushed to its registry,
    • a private project with a container image,
    • a public project whose container registry visibility is set to Only Project Members (Settings > General > Visibility, project features, permissions), with a container image.
  3. In a signed-out (incognito) session, visit the group:
    • Before: Deploy > Container Registry is hidden and /groups/<group>/-/container_registries returns 404.
    • After: the menu entry is visible and the page lists only the image from the first project. Images from the private project and from the members-only registry are not listed.
  4. Repeat step 3 signed in as a user who is not a member of the group.
  5. Repeat with a private group: non-members and signed-out users still get a 404.
  6. Optional: GET /api/v4/groups/<id>/registry/repositories without a token returns the same filtered list for a public group.

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 Ben Bodenmiller

Merge request reports

Loading
Loading