Skip group allowlist entries for own-project job tokens

What does this MR do and why?

Jobs that access their own project no longer fail with 403 when the project allowlists its own group with fine-grained permissions. The fix is behind the ci_job_token_skip_group_entries_for_own_project feature flag, disabled by default.

Problem

Currently on master, a project's own job token is restricted by fine-grained permissions on a group entry of its allowlist. This affects projects that add their own group, or a parent group, to their allowlist with fine-grained permissions.

Jobs then fail when they access their own project. A job downloading artifacts from an earlier job in the same pipeline gets 403 Forbidden - Insufficient permissions to access this resource in project #<id>. The following token permission is required: read_jobs.

Merge request 192395 narrowed the self-access exemption. It now applies only when no allowlist entry matches. A group entry for one of the project's own groups counts as a match, so the exemption does not apply.

Today the workaround is to edit the Current project entry and save it with default permissions. Alternatively, grant the needed permissions on the group entry.

Review notes

  • Why not use the fix proposed in work item 570781, scope.source_project != accessed_project? Using it changes nothing. Every entry the lookup returns belongs to the accessed project's own allowlist. The condition is therefore always false.
  • Why skip group entries and not restore the old unconditional self exemption? Restoring it would undo !192395 (merged). An explicit entry for the project itself must keep restricting its own token.
  • Why put this behind a feature flag? The change loosens what a job token can do in its own project. The flag lets group::authorization roll it out gradually on GitLab.com. It also turns the change off without a deploy if a project relied on the old restriction.
  • Does this change what a project can restrict? Yes. With the flag on, a project that limited its own token through an own-group or ancestor-group entry loses that limit. It has to restrict the token through its Current project entry instead. The UI already shows the current project with default permissions when it has no own entry.
  • Does this change any SQL? No new or altered statement. With the flag on, own-project access skips the group lookups. The capture is for a project in a subgroup with an own-group entry granting only read_packages. The remaining project-link lookup uses the unique index on source project, target project, and direction. The statements are under Queries before and after.

🛠️ with ❤️ at Siemens

References

Closes #570781

Closes #593562

  • Related: #602977 (same bug on the container registry path)
  • Epic: &17222

Screenshots or screen recordings

No UI change.

Queries before and after

Own-project access with an own-group entry and no entry for the project itself.

Before (flag off, 5 queries)
-- main, Project#ci_inbound_job_token_scope_enabled?
SELECT "project_ci_cd_settings".* FROM "project_ci_cd_settings"
WHERE "project_ci_cd_settings"."project_id" = 315 LIMIT 1;

-- ci, Allowlist#nearest_scope_for_target_project (project link)
SELECT "ci_job_token_project_scope_links".* FROM "ci_job_token_project_scope_links"
WHERE "ci_job_token_project_scope_links"."source_project_id" = 315
  AND "ci_job_token_project_scope_links"."direction" = 1
  AND "ci_job_token_project_scope_links"."target_project_id" = 315
ORDER BY "ci_job_token_project_scope_links"."id" ASC LIMIT 1;

-- main, Project#parent_groups (loads project.group)
SELECT "namespaces".* FROM "namespaces"
WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 446 LIMIT 1;

-- main, Allowlist#group_links_for_target: parent_groups.pluck(:id)
WITH RECURSIVE "base_and_ancestors" AS (
  (SELECT "namespaces".* FROM "namespaces"
   WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 446)
  UNION
  (SELECT "namespaces".* FROM "namespaces", "base_and_ancestors"
   WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = "base_and_ancestors"."parent_id")
) SELECT "id" FROM "base_and_ancestors" AS "namespaces";

-- ci, Allowlist#nearest_scope_for_target_project (group link fallback)
SELECT "ci_job_token_group_scope_links".* FROM "ci_job_token_group_scope_links"
WHERE "ci_job_token_group_scope_links"."source_project_id" = 315
  AND "ci_job_token_group_scope_links"."target_group_id" IN (446, 445)
ORDER BY array_position(ARRAY[446, 445]::bigint[], ci_job_token_group_scope_links.target_group_id)
LIMIT 1;

The real statements list every namespaces column. They are shortened to "namespaces".* here.

After (flag on, 2 queries)
-- main, Project#ci_inbound_job_token_scope_enabled?
SELECT "project_ci_cd_settings".* FROM "project_ci_cd_settings"
WHERE "project_ci_cd_settings"."project_id" = 315 LIMIT 1;

-- ci, Scope#nearest_scope (self link only)
-- index: ci_job_token_scope_links_source_and_target_project_direction (unique)
SELECT "ci_job_token_project_scope_links".* FROM "ci_job_token_project_scope_links"
WHERE "ci_job_token_project_scope_links"."source_project_id" = 315
  AND "ci_job_token_project_scope_links"."direction" = 1
  AND "ci_job_token_project_scope_links"."target_project_id" = 315
ORDER BY "ci_job_token_project_scope_links"."id" ASC LIMIT 1;

How to set up and validate locally

  1. In the Rails console, enable the feature flag.
    Feature.enable(:ci_job_token_skip_group_entries_for_own_project)
  2. Pick a project in a group, enable the inbound job token scope, and add the group with only read_packages.
    project = Project.find_by_full_path('<group>/<project>')
    project.ci_cd_settings.update!(inbound_job_token_scope_enabled: true)
    Ci::JobToken::Allowlist.new(project).add_group!(project.group, user: User.first, default_permissions: false, policies: %w[read_packages])
  3. Check own-project access. It returns true.
    Ci::JobToken::Scope.new(project).policies_allowed?(project, [:read_jobs])

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 Gerardo Navarro

Merge request reports

Loading
Loading