Broker: resolve username->email via Cloudron user list (proxyAuth injects username, not email)

This commit is contained in:
inference-bot committed 2026-09-14 12:59:42 -06:00
1 parent a46c016817
commit 3024bfec74
1 file changed
+42 -5
+42 -5
View File
@@ -1102,23 +1102,60 @@ async def litellm_delete_keys_by_alias(email: str) -> None:
def _verify_broker_secret(request: Request) -> str:
"""Return the authenticated member's email, or 401.
The member dashboard sends X-Broker-Secret (shared secret) + X-Member-Email.
Both must be present and the secret must match, or we refuse (fail closed).
The member dashboard sends X-Broker-Secret (shared secret) + X-Member-Email
(or X-Member-User, a Cloudron username). Both must be present and the secret
must match, or we refuse (fail closed).
Cloudron's proxyAuth injects the USERNAME (e.g. "ntnsndr"), not the email.
So when the dashboard sends a username (no "@"), we resolve it to an email
via the Cloudron user list (we already hold a Cloudron admin token).
"""
if not BROKER_SECRET:
raise HTTPException(503, "Broker secret not configured")
provided = request.headers.get("x-broker-secret", "")
if not secrets.compare_digest(provided, BROKER_SECRET):
raise HTTPException(401, "Invalid broker secret")
email = (request.headers.get("x-member-email") or "").strip().lower()
if not email:
raise HTTPException(401, "Missing member email")
identity = (
request.headers.get("x-member-email")
or request.headers.get("x-member-user")
or ""
).strip()
if not identity:
raise HTTPException(401, "Missing member identity")
email = resolve_member_email(identity)
# The member must be active in our DB before they can manage keys.
if not get_member_key(email):
raise HTTPException(403, "No active membership")
return email
def resolve_member_email(identity: str) -> str:
"""Resolve a member identity (email OR Cloudron username) to their email.
If it looks like an email (contains "@"), return it lowercased. Otherwise
treat it as a Cloudron username and look up the corresponding email from
the Cloudron user list (synchronous, using urllib since this runs outside
the async request path of httpx).
"""
identity = identity.strip()
if "@" in identity:
return identity.lower()
# Username → email via Cloudron user list.
import urllib.request
req = urllib.request.Request(
f"{CLOUDRON_API}/api/v1/users",
headers={"Authorization": f"Bearer {CLOUDRON_TOKEN}"},
)
with urllib.request.urlopen(req, timeout=15) as r:
data = json.loads(r.read().decode())
users = data.get("users", [])
for u in users:
if u.get("username") == identity:
return (u.get("email") or "").lower()
raise HTTPException(403, f"No Cloudron user for identity {identity!r}")
@app.get("/broker/keys")
async def broker_list(request: Request):
email = _verify_broker_secret(request)