Pass the subject root namespace to the Code Suggestions seat check

What does this MR do and why?

This MR is a follow-up to merge request !251601 (merged). That MR moved the Code Suggestions availability check into ProjectPolicy and GroupPolicy, as the :access_code_suggestions ability.

The check itself is the condition code_suggestions_enabled_for_user. It lives in a shared module, Ai::DuoAgentPlatform::AccessPolicy, in the file ee/app/policies/ai/duo_agent_platform/access_policy.rb. Both ProjectPolicy and GroupPolicy include this module.

Before this MR, the condition called @user.allowed_to_use?(:code_suggestions) without a root_namespace. Duo Chat already passes root_namespace: @subject.root_ancestor in its own check. This MR makes Code Suggestions do the same, so both features work the same way.

The condition also drops scope: :user. The reason is simple: the result now depends on the subject (the project or the group), not only on the user. So it can no longer be cached per user alone.

The old code comment that pointed to the follow-up issue is removed, since the follow-up is now done.

Behavior change

The seat check does not change. allowed_to_use? still looks at all active Duo seats a user has, in any group. So a user with a seat in group A can still use Code Suggestions in a project that is in group B. This part stays the same.

What changes is which group's "Restrict access based on group membership" rules apply. These are the Ai::NamespaceFeatureAccessRule records for GitLab Duo Non-Agentic. In the code, this is called duo_classic.

Before this MR, only the rules of the user's default GitLab Duo namespace applied.

After this MR, if the user is a member of the top-level group of the project or group being checked, that top-level group's rules apply instead. If the user is not a member of that top-level group, nothing changes. The default Duo namespace rules still apply in that case.

Here is an example. A user has a seat in group A. Group B restricts Duo Non-Agentic to members of a subgroup, B/allowed. The user is a member of group B, but not of B/allowed.

  • Before this MR: Code Suggestions in group B projects was allowed.
  • After this MR: it is blocked.
  • If the user is a member of B/allowed, access stays allowed.
  • If the user is not a member of group B at all, access also stays allowed.

This new behavior matches Duo Chat. It also matches the current documentation on the page "Configure access to GitLab Duo": https://docs.gitlab.com/administration/gitlab_duo/configure/access_control/. That page says the rules apply to direct members of a configured group, when they use an AI feature in a project or subgroup under that group's top-level group. So this MR makes the code match the docs. No docs change is needed.

Identity verification is not affected by this change. That check only applies to GitLab Duo Agent Platform features.

References

How to set up and validate locally

  1. Seed a Duo group and project in SaaS mode. Run this from the Rails root:
    GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[duo_pro]'
    This creates the group gitlab-duo and gives the root user a Duo Pro seat in it.
  2. Start GDK with SaaS simulation turned on.
  3. Open a Rails console. Create a second top-level group with a project, add root as a guest, and check the policy. Use this Ruby code:
    user = User.find_by_username('root')
    other_group = Group.create!(name: 'other-group', path: 'other-group', organization: user.organization)
    other_group.namespace_settings.update!(duo_availability: 'default_on')
    project = Projects::CreateService.new(user, name: 'other-project', path: 'other-project', namespace_id: other_group.id).execute
    other_group.add_guest(user)
    
    # Seat is in gitlab-duo, project is in other-group, no rules: allowed
    Ability.allowed?(user, :access_code_suggestions, project) # => true
    
    # other-group restricts Duo Non-Agentic to members of a subgroup. root is not in it: blocked
    allowed_subgroup = Group.create!(name: 'allowed', path: 'allowed', parent: other_group, organization: user.organization)
    Ai::NamespaceFeatureAccessRule.create!(root_namespace: other_group, through_namespace: allowed_subgroup, accessible_entity: 'duo_classic')
    Ability.allowed?(User.find(user.id), :access_code_suggestions, Project.find(project.id)) # => false
    
    # Add root to the allowed subgroup: allowed again
    allowed_subgroup.add_guest(user)
    Ability.allowed?(User.find(user.id), :access_code_suggestions, Project.find(project.id)) # => true
    Note: allowed_to_use? caches its result in Rails.cache and in the request store. You may need to reload the user and the project, or run Rails.cache.clear, between checks.

Tests:

  • Two existing specs stubbed allowed_to_use? with (:code_suggestions). They now expect the extra root_namespace: argument. Files: ee/spec/policies/project_policy_spec.rb and ee/spec/policies/ee/group_policy_spec.rb.
  • New specs were added in both files, for a user with a seat in group A and a subject (project or group) in group B:
    • group B has no access rules: allowed
    • group B has rules, but the user is not a member of group B: allowed
    • group B has rules, the user is a member of group B but not of the allowed subgroup: not allowed
    • the user is a member of the allowed subgroup: allowed
    • one spec checks that allowed_to_use? gets root_namespace: equal to the subject's root ancestor
  • Results: the access_code_suggestions policy specs in both files pass, 27 examples, 0 failures. When the policy change is reverted, exactly 4 of the new examples fail, which confirms they test the right thing.
  • ee/spec/requests/api/code_suggestions_spec.rb: 192 examples, 0 failures, 11 pending.

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.

Merge request reports

Loading
Loading