From 64d3c6aa837a29bde30b6afa903cdda9af2633fe Mon Sep 17 00:00:00 2001 From: inference-bot Date: Tue, 29 Sep 2026 21:53:46 -0600 Subject: [PATCH] =?UTF-8?q?Broker:=20resolve=20uid-=20identities=20(OIDC?= =?UTF-8?q?=20sub)=20to=20email=20=E2=80=94=20email-invited=20users=20have?= =?UTF-8?q?=20no=20username?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index abbf3f0..5d47d31 100644 --- a/app/main.py +++ b/app/main.py @@ -2042,17 +2042,18 @@ def _verify_broker_secret(request: Request) -> str: def resolve_member_email(identity: str) -> str: - """Resolve a member identity (email OR Cloudron username) to their email. + """Resolve a member identity (email, Cloudron username, or uid) to 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). + Cloudron's OIDC `sub` claim is the user ID (uid-…), and email-invited users + have no username at all. So accept three identity shapes: + - email ("@") → return as-is + - uid-… → match on the user id + - username → match on the username field (legacy accounts) """ identity = identity.strip() if "@" in identity: return identity.lower() - # Username → email via Cloudron user list. + # uid / username → email via Cloudron user list (one fetch covers both). import urllib.request req = urllib.request.Request( @@ -2062,6 +2063,9 @@ def resolve_member_email(identity: str) -> str: 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("id") == identity: + return (u.get("email") or "").lower() for u in users: if u.get("username") == identity: return (u.get("email") or "").lower()