Broker: resolve username->email via Cloudron user list (proxyAuth injects username, not email)
This commit is contained in:
1 parent
a46c016817
commit
3024bfec74
1 file changed
+42
-5
+42
-5
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user