feat(billing): add CustomersDot usage quota gate for mcp/rest queries

Summary

Adds a pre-execution usage quota gate for mcp and rest source types. Before executing a query the server sends HEAD /api/v1/consumers/resolve to CustomersDot (with X-Admin-Email / X-Admin-Token credentials) and caches the decision in-process. Requests for frontend, core, and dws bypass the gate entirely.

CDot response → GKG decision → cache behaviour

CDot status GKG decision gRPC status returned Cached TTL
200 OK Allow (query runs) Yes Cache-Control: max-age or 3600 s fallback
402 Payment Required Deny – quota exhausted RESOURCE_EXHAUSTED Yes Cache-Control: max-age or 3600 s fallback
403 Forbidden Allow (fail-open) (query runs) No -
Any other 4xx / 5xx Allow (fail-open) (query runs) No -
Connection error / timeout Allow (fail-open) (query runs) No -

Only 200 and 402 outcomes are cached (per-pod moka, max 10 000 entries, TTL ± 10 % jitter). Fail-open outcomes are never cached. Each request retries CDot.

Configuration

billing:
  quota:
    enabled: true                          # switch to enable access cut-off check
    customers_dot_url: "http://localhost:5000"
    api_user: "<CDOT_ADMIN_USER>"
    api_token: "<CDOT_ADMIN_TOKEN>"
    request_timeout_ms: 1000

If api_user or api_token is empty while enabled: true, the gate disables itself with a warning to avoid silent fail-open on 401.

Steps to reproduce (local end-to-end)

Prerequisites

  • GDK running (Rails on port 3000, CDot on port 5000, ClickHouse on port 9001)
  • Orbit billable namespace set as per instructions here - gitlab-org/gitlab!233102 (merged).
  • CDot running. Admin user created (<CDOT_ADMIN_USER> / <CDOT_ADMIN_TOKEN>)

Create a personal access token

# In the GDK directory
mise exec -- bin/rails runner "
  user = User.find_by(username: '<YOUR-GITLAB-USERNAME>')
  pat = user.personal_access_tokens.create!(
    name: 'gkg-quota-test',
    scopes: ['api'],
    expires_at: 1.day.from_now
  )
  puts pat.token
"

Save the printed token as <YOUR-PAT>.

Start GKG with quota enabled

GKG_BILLING__QUOTA__ENABLED=true \
GKG_BILLING__QUOTA__CUSTOMERS_DOT_URL=http://localhost:5000 \
GKG_BILLING__QUOTA__API_USER=<CDOT_ADMIN_USER> \
GKG_BILLING__QUOTA__API_TOKEN=<CDOT_ADMIN_TOKEN> \
mise server:start

Allow path (CDot returns 200)

Use a PAT belonging to a namespace that has an active CDot subscription:

curl -s -X POST http://localhost:3000/api/v4/orbit/query \
              -H "Authorization: Bearer <PAT>" \
              -H "Content-Type: application/json" \
              -d '{
      "query": {
              "query_type": "neighbors",
              "node": {
                "id": "me",
                "entity": "User",
                "node_ids": [
                  22
                ]
              },
              "neighbors": {
                "node": "me",
                "direction": "both"
              },
              "limit": 100
            },
      "format": "raw"
    }'

Expected: 200 OK with query results.

Deny path (CDot returns 402)

  1. No subscription - the namespace has no CDot subscription at all.
  2. Quota exhausted - the namespace has a subscription but credits are depleted. This cannot be tested currently because we need to add to configure the orbit_workflow_completion event as billable on CDot.

Use a PAT belonging to a user who has as Orbit billable namespace with with no CDot subscription:

curl -s -X POST http://localhost:3000/api/v4/orbit/query \
              -H "Authorization: Bearer <PAT>" \
              -H "Content-Type: application/json" \
              -d '{
      "query": {
              "query_type": "neighbors",
              "node": {
                "id": "me",
                "entity": "User",
                "node_ids": [
                  22
                ]
              },
              "neighbors": {
                "node": "me",
                "direction": "both"
              },
              "limit": 100
            },
      "format": "raw"
    }'

Expected: 402 Payment Required with {"code":"quota_exhausted","message":"GitLab credits exhausted"} (after the Workhorse MR merges).

Bypass path (non-metered source types)

The source_type claim in the JWT is set by Rails based on the calling context. For frontend, core, or dws callers, no CDot call is made regardless of subscription state.

Fail-open path

Point customers_dot_url at a non-existent host or a stub returning 5xx. Expect all queries to proceed.

GKG_BILLING__QUOTA__ENABLED=true \
GKG_BILLING__QUOTA__CUSTOMERS_DOT_URL=http://127.0.0.1:1 \
GKG_BILLING__QUOTA__API_USER=<CDOT_ADMIN_USER> \
GKG_BILLING__QUOTA__API_TOKEN=<CDOT_ADMIN_TOKEN> \
mise server:start

Expected: queries succeed; GKG logs quota check request failed; failing open.

Related to https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/work_items/487

Edited by Sharmad Nachnolkar

Merge request reports

Loading
Loading