Draft: Add pipeline execution policy bot access settings for CI config projects

What does this MR do and why?

This MR allows CI config project owners to grant security policy bots access to specific CI/CD configuration files. This enables scheduled pipeline execution policies to reference CI configs stored outside the security policy project.

Problem

Currently, security policy bots can only access CI/CD configuration files stored in the security policy project itself. When a scheduled pipeline execution policy references a CI config from a different project, the bot doesn't have access and the pipeline fails.

Solution

Add new project settings that allow CI config project owners to explicitly grant access to security policy bots:

  • pipeline_execution_policy_bot_access_enabled: Toggle to enable bot access
  • pipeline_execution_policy_bot_access_file_patterns: Glob patterns specifying which files can be accessed (e.g., policy-ci.yml, security/**/*.yml)
  • pipeline_execution_policy_bot_access_group_id: Restrict access to bots from projects within a specific group (defaults to the CI config project's root group)

Security Model

  1. CI config project owner controls access - They explicitly opt-in and decide which files are exposed
  2. File-level granularity - Only specific files matching the patterns, not the entire repository
  3. Group restriction - Only bots from projects within the allowed group hierarchy can access
  4. Fail closed - Ambiguous cases (bot with no project, multiple projects, etc.) deny access

Edge Cases Handled

Edge Case Behavior
Bot has no project membership Deny access
Bot has multiple project memberships Deny access
CI config project in personal namespace Deny access (no default group)
Allowed group deleted Fall back to project's root group
Empty file patterns with feature enabled Deny access
Path with .. or leading / Deny access

Here's a step-by-step guide to test this locally using the Rails console:


Local testing steps

1. Run migrations

bundle exec rails db:migrate

2. Set up test data

Open the Rails console:

bundle exec rails console

Create the test setup:

# Create a group and subgroup
group = Group.find_or_create_by!(name: 'test-group', path: 'test-group')
subgroup = Group.find_or_create_by!(name: 'test-subgroup', path: 'test-subgroup', parent: group)

# Create the CI config project (where the CI config files live)
ci_config_project = Project.find_by_full_path('test-group/ci-config-project') ||
  Projects::CreateService.new(User.first, { name: 'ci-config-project', namespace: group }).execute

# Create a target project (where the policy is enforced)
target_project = Project.find_by_full_path('test-group/test-subgroup/target-project') ||
  Projects::CreateService.new(User.first, { name: 'target-project', namespace: subgroup }).execute

# Create a security policy bot for the target project
bot = User.create!(
  name: 'Security Policy Bot',
  username: "gitlab_security_policy_project_#{target_project.id}_bot",
  email: "security_bot_#{target_project.id}@noreply.gitlab.com",
  user_type: :security_policy_bot,
  skip_confirmation: true,
  projects_limit: 0
)

# Add bot as member of target project
target_project.add_guest(bot)

3. Configure CI config project to allow bot access

# Enable bot access with file patterns
ci_config_project.project_setting.update!(
  pipeline_execution_policy_bot_access_enabled: true,
  pipeline_execution_policy_bot_access_file_patterns: ['policy-ci.yml', 'security/**/*.yml']
)

4. Test the access check

# This should return true
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'policy-ci.yml',
  bot_project: target_project
)

# This should return true (glob pattern)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'security/scans/sast.yml',
  bot_project: target_project
)

# This should return false (file not in patterns)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'other-file.yml',
  bot_project: target_project
)

# This should return false (path traversal)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: '../secret.yml',
  bot_project: target_project
)

5. Test group restriction

# Create a project outside the allowed group
other_group = Group.find_or_create_by!(name: 'other-group', path: 'other-group')
other_project = Project.find_by_full_path('other-group/other-project') ||
  Projects::CreateService.new(User.first, { name: 'other-project', namespace: other_group }).execute

# Create bot for other project
other_bot = User.create!(
  name: 'Other Security Policy Bot',
  username: "gitlab_security_policy_project_#{other_project.id}_bot",
  email: "security_bot_#{other_project.id}@noreply.gitlab.com",
  user_type: :security_policy_bot,
  skip_confirmation: true,
  projects_limit: 0
)
other_project.add_guest(other_bot)

# This should return false (bot's project not in allowed group)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'policy-ci.yml',
  bot_project: other_project
)

6. Test custom group restriction

# Restrict to subgroup only
ci_config_project.project_setting.update!(
  pipeline_execution_policy_bot_access_group_id: subgroup.id
)

# This should still return true (target_project is in subgroup)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'policy-ci.yml',
  bot_project: target_project
)

# Create project directly in parent group
parent_group_project = Project.find_by_full_path('test-group/parent-project') ||
  Projects::CreateService.new(User.first, { name: 'parent-project', namespace: group }).execute

parent_bot = User.create!(
  name: 'Parent Security Policy Bot',
  username: "gitlab_security_policy_project_#{parent_group_project.id}_bot",
  email: "security_bot_#{parent_group_project.id}@noreply.gitlab.com",
  user_type: :security_policy_bot,
  skip_confirmation: true,
  projects_limit: 0
)
parent_group_project.add_guest(parent_bot)

# This should return false (parent_group_project is not in subgroup)
ci_config_project.project_setting.allows_pipeline_execution_policy_bot_access?(
  file_path: 'policy-ci.yml',
  bot_project: parent_group_project
)

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Andy Schoenen

Merge request reports

Loading
Loading