Record Step 1 findings: member dashboard must route all LiteLLM access through portal

This commit is contained in:
inference-bot committed 2026-09-14 08:53:27 -06:00
1 parent 176628d2c0
commit 7d572ed793
1 file changed
+23 -4
+23 -4
View File
@@ -118,11 +118,30 @@ Build order with dependencies and verification gates. Each step must be verified
### Step 1 — Confirm spend-log scoping (the riskiest unknown)
*Resolves the "Risks" item that could invalidate the whole member dashboard.*
**COMPLETED (2026-09-13). Outcome: the member dashboard cannot read LiteLLM directly — all reads AND writes must go through the broker/portal.**
- Query `/spend/logs` and `/user/info` filtered by a single key/team; confirm a team-scoped request returns **only that member's** rows, not everyone's.
- Also confirm the broker can read *only* the target member's team (no cross-team enumeration).
- **Gate:** if scoping isn't clean, the member dashboard must go through the broker for *reads too*, which changes the architecture. Resolve before any build.
Findings:
- **Member keys cannot read spend endpoints.** A member's own key (both `sk-` virtual keys and legacy raw-hash tokens) gets `401 "Only proxy admin"` on `/spend/logs`, `/spend/keys`, and `/user/info`. Only the master key can query spend.
- **Consequence:** the member dashboard must route *reads* through the portal (which holds the master key) too, not just writes. This actually *simplifies* the security model — the member dashboard holds **no LiteLLM credential at all**; it just calls the portal, which already authenticates members and already holds the master key.
- **`/spend/keys` is the clean per-member read primitive** — it returns one row per key with `key_alias` (`member:<email>`), `spend`, and `team_id`. Filtering by alias gives a member's spend directly.
- **`/spend/logs` filtering is inconsistent:** `api_key=<hash>` filters correctly, but `team_id=<x>` is silently ignored (returned all 92 rows for a bogus team id). Do not rely on `team_id` filtering on `/spend/logs`; prefer `/spend/keys` + `key_alias`.
- **Data inconsistency found (must fix in Step 2):** three members (Dan, Ed, Nathan) have **raw-hash key tokens** (not valid LiteLLM virtual keys), while four (Lee, Joseph, Liz, Roz) have `sk-` keys. The raw-hash tokens are a legacy artifact; the Teams migration will recreate all keys consistently as `sk-` virtual keys.
- **`/user/info?user_id=`** works with the master key but shows `spend: 0` and empty `keys`/`teams` because current keys have `user_id: null` (key-only, not user-bound). After the Teams migration binds keys to teams, per-team spend becomes queryable.
Revised architecture (all LiteLLM access via the portal):
```
Member (browser) ──SSO──▶ Member Dashboard (new Cloudron app, "members" group)
│ holds NO LiteLLM/Cloudron/OC credentials
│ calls the portal for everything
▼
Portal (holds master key; authenticates member)
│ read: /spend/keys?key_alias=member:<email>
│ write: mint/revoke key via master key
▼
LiteLLM Gateway (Teams + budgets + spend logs)
```
### Step 2 — Teams migration (Phase 1)