Add upsertUserBudgetCapOverrides GraphQL mutation

What does this MR do and why?

Adds the upsertUserBudgetCapOverrides GraphQL mutation for bulk upserting per-user budget cap overrides. Accepts GlobalIDType[User] IDs, validates users exist, translates to CDot entityId strings, and proxies to CDot.

Includes a max 200 overrides limit, UserOverrideInputType, and 13 request specs covering SaaS, self-managed, unauthorized, feature flag, and edge cases.

Gated behind the budget_caps_graphql_api feature flag.

Related to https://gitlab.com/gitlab-org/gitlab/-/issues/595491

Important: CDot prerequisite

A subscription-level flat user cap must exist in CDot before user overrides can be upserted. If no flat cap record exists, CDot returns:

{ "errors": ["Subscription budget cap not found"] }

This means in practice, the upsertFlatUserCap mutation (MR3) must be called first to create the subscription budget cap record, then upsertUserBudgetCapOverrides can be used to set per-user overrides.

Stacked MR

# MR Description Status
1 !230214 (merged) Foundation — feature flag + client + shared concern Merged
2 !230218 (merged) Query — budget caps read API (BudgetCapsType, UserOverrideType, PORO) Merged
3 !230224 (merged) Mutation — upsert flat user cap In review
4 This MR Mutation — upsert user budget cap overrides You are here
5 !230226 (closed) Enable feature flag by default (draft, blocked by 3-4) Draft

How to set up and validate locally

Prerequisites: GDK running with CDot on localhost:5000. Use existing group saastestgroup.

  1. Enable the feature flag

    Feature.enable(:budget_caps_graphql_api)
  2. Ensure a flat cap exists in CDot

    A subscription budget cap record must exist before user overrides can be added. Either use the upsertFlatUserCap mutation (MR3) or seed directly in CDot console:

    BudgetCap::Subscription.create!(
      subscription_name: "A-S00409070",
      deployment_type: :saas,
      subscription_cap: 5000,
      subscription_cap_enabled: true,
      flat_user_cap: 200.0,
      flat_user_cap_enabled: true
    )

    Without this, the mutation returns: "Subscription budget cap not found"

  3. SaaS — Owner upserts user overrides

    Log in as owner of saastestgroup. Open GraphiQL at http://localhost:3000/-/graphql-explorer:

    mutation {
      upsertUserBudgetCapOverrides(input: {
        namespacePath: "saastestgroup"
        overrides: [
          { userId: "gid://gitlab/User/1", cap: 300.0, enabled: true }
          { userId: "gid://gitlab/User/3", cap: 150.0, enabled: false }
        ]
      }) {
        userOverrides {
          user { id username }
          cap
          capEnabled
        }
        errors
      }
    }

    Expected: updated overrides with resolved User objects.

  4. Self-Managed — Instance admin upserts overrides

    Log in as admin. Admin mode must be enabled — toggle admin mode from user settings (click avatar → Preferences → enable Admin Mode), or use a Personal Access Token (PAT) with api scope which bypasses admin mode automatically via sessionless_bypass_admin_mode!.

    Omit namespacePath for the SM path:

    mutation {
      upsertUserBudgetCapOverrides(input: {
        overrides: [
          { userId: "gid://gitlab/User/1", cap: 500.0, enabled: true }
        ]
      }) {
        userOverrides {
          user { id username }
          cap
          capEnabled
        }
        errors
      }
    }

    Note: On Self-Managed, the admin policy condition in BasePolicy requires both user.admin? and an active admin mode session (Gitlab::Auth::CurrentUserMode#admin_mode?). Without admin mode enabled, the mutation returns a permission error. API requests authenticated with a PAT bypass this requirement automatically.

  5. Invalid userId

    mutation {
      upsertUserBudgetCapOverrides(input: {
        namespacePath: "saastestgroup"
        overrides: [
          { userId: "gid://gitlab/User/999999", cap: 100.0, enabled: true }
        ]
      }) {
        userOverrides { user { id } }
        errors
      }
    }

    Expected: error "One or more specified users were not found"

  6. Unauthorized — non-owner (SaaS)

    Log in as non-owner, run mutation from step 3. Expected: permission error.

  7. Feature flag disabled

    Feature.disable(:budget_caps_graphql_api)

    Run mutation from step 3 again. Expected: error about feature flag being disabled.

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 Suraj Tripathi

Merge request reports

Loading