Add budget caps GraphQL query types and PORO layer

What does this MR do and why?

Adds the GraphQL query layer for reading budget cap controls via subscriptionUsage { budgetCaps { ... } }. This proxies budget cap data from the Customer Portal (CDot) through GitLab's public GraphQL API.

Users (namespace owners on SaaS, instance admins on SM) can now query:

  • Subscription-level cap and flat per-user cap
  • Per-user budget cap overrides with resolved GitLab User objects
  • Relay-style pagination and userIds filtering on overrides

Gated behind the budget_caps_graphql_api feature flag (disabled by default, introduced in MR1).

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

Stacked MR

# MR Description Status
1 !230214 (merged) Foundation — feature flag + client + shared concern Merged
2 This MR Query — budget caps read API (BudgetCapsType, UserOverrideType, PORO) Merged
3 !230224 (merged) Mutation — upsert flat user cap In review
4 !230222 (merged) Mutation — upsert user budget cap overrides In review
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 (CUSTOMER_PORTAL_URL=http://localhost:5000 in gdk.yml). Use an existing group/subscription that has budget cap data seeded in CDot.

1. Enable the feature flag

# Rails console
Feature.enable(:budget_caps_graphql_api)

2. SaaS — Root namespace owner gets budget caps

Log in as the owner of an existing root group (e.g., saastestgroup). Open GraphiQL at http://localhost:3000/-/graphql-explorer and run:

{
  subscriptionUsage(namespacePath: "saastestgroup") {
    budgetCaps {
      subscriptionCap
      subscriptionCapEnabled
      flatUserCap
      flatUserCapEnabled
      userOverrides(first: 2) {
        nodes {
          user {
            id
            username
            name
          }
          cap
          capEnabled
          createdAt
          updatedAt
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  }
}

Expected: budgetCaps returns subscription-level cap fields and paginated userOverrides with resolved User objects.

3. SaaS — Pagination

Use the endCursor from step 2:

{
  subscriptionUsage(namespacePath: "saastestgroup") {
    budgetCaps {
      userOverrides(first: 2, after: "<endCursor from step 2>") {
        nodes {
          user {
            id
            username
          }
          cap
          capEnabled
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  }
}

Expected: Next page of overrides, hasNextPage reflects remaining data.

4. SaaS — Filter by userIds

{
  subscriptionUsage(namespacePath: "saastestgroup") {
    budgetCaps {
      userOverrides(userIds: ["gid://gitlab/User/1"]) {
        nodes {
          user {
            id
            username
          }
          cap
          capEnabled
        }
      }
    }
  }
}

Expected: Only the override for the specified user is returned.

5. Self-Managed — Instance admin gets budget caps

Log in as an instance 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:

{
  subscriptionUsage {
    budgetCaps {
      subscriptionCap
      subscriptionCapEnabled
      flatUserCap
      flatUserCapEnabled
      userOverrides(first: 2) {
        nodes {
          user {
            id
            username
            name
          }
          cap
          capEnabled
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  }
}

Expected: Budget caps returned using License.current.

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 query returns a permission error. API requests authenticated with a PAT bypass this requirement automatically.

6. Unauthorized — Non-owner (SaaS)

Log in as a user who is not an owner of the group and run the query from step 2.

Expected: subscriptionUsage returns null with permission error.

7. Unauthorized — Non-admin (SM)

Log in as a non-admin user and run the query from step 5.

Expected: subscriptionUsage returns null with permission error.

8. Feature flag disabled

Feature.disable(:budget_caps_graphql_api)

Re-run any query above. Expected: budgetCaps returns null, other subscriptionUsage fields still work.

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