# FinOps Query Gateway — Operator Runbook

Administration guide for the FinOps Query Gateway: token issuance, grant management, audit queries, and troubleshooting.

## Token Issuance

Tokens authenticate users against the Gateway API. Only admins can issue tokens.

```bash
# Issue an API token for a user
curl -X POST http://localhost:4000/v1/admin/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_email": "user@example.com", "name": "dev-token", "expires_in_days": 90}'

# Response includes the raw token — save it, it will NOT be shown again
# Token format: fqg_<random>
# {
#   "id": "tok_abc123",
#   "token": "fqg_live_xxxxxxxxxxxxxxxxxxxxxxxx",
#   "name": "dev-token",
#   "user_email": "user@example.com",
#   "expires_at": "2026-06-11T00:00:00Z"
# }
```

To revoke a token:

```bash
curl -X DELETE http://localhost:4000/v1/admin/tokens/<token-id> \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

## Grant Management

Grants control which cloud accounts a user can query. Without a grant, the user sees no data for that account.

```bash
# Grant access to a specific cloud account
curl -X POST http://localhost:4000/v1/admin/grants \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_email": "user@example.com", "cloud_provider": "aws", "account_id": "123456789012", "expires_in_days": 30}'

# Response:
# {
#   "id": "gra_xyz789",
#   "user_email": "user@example.com",
#   "cloud_provider": "aws",
#   "account_id": "123456789012",
#   "expires_at": "2026-04-12T00:00:00Z"
# }
```

Revoke a grant:

```bash
curl -X DELETE http://localhost:4000/v1/admin/grants/<grant-id> \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

## Audit Queries

Review query execution history for compliance and debugging.

```bash
# Recent audit logs (default: last 20)
curl "http://localhost:4000/v1/admin/audit?limit=20" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Filter by user
curl "http://localhost:4000/v1/admin/audit?user_id=<user-id>&limit=20" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Filter by status (pending, completed, error)
curl "http://localhost:4000/v1/admin/audit?status=error&limit=20" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Filter by date
curl "http://localhost:4000/v1/admin/audit?since=2026-03-01&limit=50" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

Each audit entry includes: `request_id`, `user_email`, `question`, `query_sql`, `status`, `bytes_scanned`, `row_count`, `created_at`, `duration_ms`.

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| 401 Unauthorized | Token expired or invalid | Issue new token via `POST /v1/admin/tokens` |
| 403 Forbidden | Not admin / no grant for account | Admin: check role. User: request grant via admin |
| 422 unsupported_query_shape | Invalid dimensions/metrics | Check `rewrite_hint` in response; verify against catalog |
| 429 budget_exceeded | Query would scan too many bytes | Narrow date range or add service/account filters |
| Empty results, no error | User has no grants for queried accounts | Check `GET /v1/catalog/accounts` for accessible accounts |
| Connection refused | Gateway not running | Start gateway: `cd finops-query-gateway && npm run dev` |
| Slow queries (>30s) | Large date range or no filters | Add `date_from`/`date_to` and service/account filters |
| `rewrite_hint` in error | LLM generated invalid SQL shape | Re-phrase the question using the hint's suggestion |

## Environment Variables Reference

| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | (required) | PostgreSQL connection string |
| `GATEWAY_PORT` | 4000 | HTTP port |
| `GATEWAY_QUERY_BYTES_LIMIT` | 10737418240 (10 GB) | Max bytes per query |
| `GATEWAY_DAILY_BYTES_LIMIT` | 53687091200 (50 GB) | Max bytes per day per user |
| `GATEWAY_MAX_ROWS` | 1000 | Max rows per response |
| `GATEWAY_TOKEN_MAX_DAYS` | 365 | Max token validity in days |
| `BIGQUERY_PROJECT_ID` | gcp-rd-research-dev | GCP project for BigQuery |
| `GOOGLE_APPLICATION_CREDENTIALS` | (optional) | Path to GCP service account JSON |
| `GOOGLE_AUTH_CLIENT_ID` | (required for login) | Google OAuth Client ID for self-service login |
| `GOOGLE_AUTH_DOMAIN` | pingcap.com | Allowed Google Workspace domain |
| `GOOGLE_AUTH_TOKEN_DAYS` | 90 | Token validity in days for Google-auth-issued tokens |
| `OPENROUTER_API_KEY` | (optional) | LLM API key (OpenRouter provider) |
| `GEMINI_API_KEY` | (optional) | LLM API key (Gemini provider) |
| `LLM_MODEL` | (optional) | LLM model override (e.g. `google/gemini-2.0-flash`) |

## Google Auth Self-Service Login

### GCP Console Setup (one-time)

1. Go to **GCP Console → APIs & Services → Credentials**
2. Create an **OAuth 2.0 Client ID** (Web application type)
3. Add authorized JavaScript origin: `https://cloudcost-api.pingcap.net`
4. Copy the Client ID and set as `GOOGLE_AUTH_CLIENT_ID` env var
5. (Optional) Restrict to PingCAP Workspace via **Workspace Admin Console → API controls**

### Self-Registered Users

- Users who sign in via Google get `viewer` role by default
- They receive an `fqg_` token valid for `GOOGLE_AUTH_TOKEN_DAYS` (default 90)
- Tokens are marked `created_via: google_auth` in the database for audit

### Managing Google-Auth Users

```bash
# Upgrade a self-registered user's role
# Use existing admin endpoints or update directly in DB:
UPDATE users SET role = 'data_admin' WHERE email = 'user@pingcap.com';

# Disable a user (blocks future logins AND API calls)
UPDATE users SET status = 'disabled' WHERE email = 'user@pingcap.com';

# Re-enable a user
UPDATE users SET status = 'active' WHERE email = 'user@pingcap.com';
```

### Audit

Self-service tokens are distinguishable from admin-issued tokens:

```sql
-- List all self-service tokens
SELECT u.email, t.token_prefix, t.created_at, t.expires_at
FROM api_tokens t JOIN users u ON t.user_id = u.id
WHERE t.created_via = 'google_auth'
ORDER BY t.created_at DESC;
```
