Fix Code Suggestions ignoring group Duo availability
What does this MR do and why?
On GitLab.com, a group Owner can turn GitLab Duo availability off for their group. Today this does not stop Code Suggestions. Members of the group keep getting completions, and the usage is still billed to that namespace. A customer reported unexpected credit usage caused by this.
Why it happens
Nothing in the Code Suggestions permission path reads duo_features_enabled:
:access_code_suggestionsis a global ability. Its only availability check isai_features_banned, which reads the instance-levelduo_never_on?. On GitLab.com that value is alwaysfalse.code_suggestions_enabled_for_usercallsallowed_to_use?(:code_suggestions). This method looks at add-on seats, Duo Core, credits and free access, but it never readsduo_features_enabledon any branch.- Duo Chat enforces the setting in the subject-scoped
GroupPolicyandProjectPolicyrules, where the resource is known. Code Suggestions has no equivalent, so the namespace setting is never used.
The cascading setting itself works correctly. It is simply never read.
This is an accidental regression, not intended behavior. The old group-level
rule was removed in 7efa62e6 together with the retired
namespace_settings.code_suggestions column. No replacement was added when
the shared duo_features_enabled setting landed later.
What this MR changes
The completions and direct_access endpoints now resolve the namespace that
the request belongs to, and return 401 when GitLab Duo is off for it. These
are the only two paths that generate suggestions or mint an AI Gateway token.
- When the client sends
project_path, the check reads the project, so a project that opted back in still works even if its parent group is off by default. Only "Always off" locks descendants. - When there is no usable
project_path, it falls back to the user's governing namespace, which is where the usage is billed. - If no namespace can be determined, the request is allowed. We cannot attribute it to an owner who turned Duo off.
The check in direct_access runs before the token is minted. The AI Gateway
signs those tokens itself and cannot revoke them, so a token issued here would
keep working, and billing, until it expires.
The change only affects GitLab.com. Both new model methods return early unless the instance is SaaS, so self-managed behavior does not change. There the instance-level setting already works as a kill switch.
Behind the block_code_suggestions_when_duo_disabled feature flag
(gitlab_com_derisk, default off). The flag actor is the root namespace, so
a whole group flips together and we can enable it for one customer at a time
with --group=.
Why the global ability is left alone
An earlier version of this MR also added a prevent rule on
:access_code_suggestions so the UI would stop advertising the feature. That was
removed after review, for two reasons.
- It does not match how Duo works elsewhere.
access_duo_classic_chatandaccess_duo_agentic_chatare subjectless and gate on user-level licensing only.duo_features_enabledis enforced at the resource layer, where the cascaded value for that exact project or group can be read. - It created an override bug. A subjectless rule can only look at each root
group's own column, so it treated an unlocked "Off by default" group as fully
off even when a project underneath had opted back in. Because the API checks
that ability in its
beforeblock, such a user got a401before the project-aware check could run.
So the setting is now enforced in exactly one place: the per-request check, which is the only place the resource is actually known.
If we do want to stop advertising Code Suggestions in the IDE for a user who is off everywhere, that is better done as a separate, project-aware availability signal.
Out of scope
Found during the investigation, and better handled separately:
- Fold the per-request check into a subject-scoped policy once the feature flag is removed, to match how Duo Chat checks access. Tracked in . A code comment on `duo_disabled_for_request?` points to this.
- No AI Gateway change needed. The gateway never reads a namespace Duo setting. It trusts the JWT scopes that the monolith creates. Namespace headers are only used for logs, Snowplow and billing context.
- Direct access tokens live for one hour. The AI Gateway signs them itself and cannot revoke them. This MR stops new tokens from being created, but a token that already exists keeps working until it expires.
strong_memoize_with(:project, …)inee/lib/api/code_suggestions.rbsets@projectto aHash.ApplicationContextexpects aProject, soprojectis missing from all Code Suggestions logs.root_namespace_from_application_contextreads the context key:group, which does not exist. Only:projectand:root_namespacedo. The code is never reached in production.never_ondoes not resetduo_agent_platform_enabled, andcheck_gitlab_credits_accessonly checks that setting forDAP_ENFORCED_UNIT_PRIMITIVES. Credit-funded Code Suggestions have no such gate. This is a product question.
References
- Issue: #606495
- Rollout issue: #607850
- Earlier discussion of the same gap: #535507 (moved)
- Commit that removed the old group-level rule: 7efa62e6
- Expected behavior is already documented here, and this MR makes the code
match it:
doc/development/ai_features/availability.md
Screenshots or screen recordings
Backend only, so there is no visual change to show. The user-visible effect is
that Code Suggestions returns 401 in the IDE for a group that has GitLab Duo
turned off.
How to set up and validate locally
completions cannot be called with plain curl, because it runs
verify_workhorse_api!. Use the Rails console and the direct_access
endpoint instead.
-
Seed a Duo group and project in GitLab.com mode. Run this from the Rails root directory:
GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[duo_pro]' -
Start GDK with SaaS simulation on, so
Gitlab::Saas.feature_available?(:gitlab_com_subscriptions)istrue. -
Open the Rails console and set up the case from the issue:
user = User.find_by_username('root') group = Group.find_by_full_path('gitlab-duo') project = group.all_projects.first Feature.enable(:block_code_suggestions_when_duo_disabled) # Before: not blocked user.code_suggestions_blocked_for?(project) # => false # Owner sets "Always off" Groups::UpdateService.new(group, user, duo_availability: 'never_on').execute # After: blocked user.code_suggestions_blocked_for?(Project.find(project.id)) # => true -
Confirm the API rejects the request. This should return
401:curl --request POST --header "PRIVATE-TOKEN: <your_token>" \ "http://gdk.test:3000/api/v4/code_suggestions/direct_access" -
Confirm that a project can still override an unlocked parent group:
Groups::UpdateService.new(group, user, duo_availability: 'default_off').execute Project.find(project.id).project_setting.update!(duo_features_enabled: true) user.code_suggestions_blocked_for?(Project.find(project.id)) # => false -
Set the group back to
default_onand check that everything works again:Groups::UpdateService.new(group, user, duo_availability: 'default_on').execute user.code_suggestions_blocked_for?(Project.find(project.id)) # => false -
Turn the feature flag off and confirm the old behavior returns:
Feature.disable(:block_code_suggestions_when_duo_disabled)
Test coverage
New tests cover: the project-scoped case, the governing-namespace fallback, an ambiguous namespace (still allowed), a locked parent that overrides a project opt-in, self-managed (no change), and the feature flag being off or enabled for a different namespace.
Two cases check that the project the request is about wins over the governing namespace, in both directions:
- A project with Duo off while the user's default Duo namespace has it on is blocked.
- A
project_pathfor a project the user cannot read with Duo off, while the governing namespace has it on, is allowed (we fall back to the governing namespace, which is what the rest of the endpoint already does).
There is also a regression guard for the override bug found in review: an unlocked
"Off by default" group with a project that opted back in still returns 200.
Local runs: 771 examples, 0 failures across the changed and related spec
files. RuboCop and scripts/lint-doc.sh are clean.
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.