From e18cd4b703ad46a3a65a21809bbe7ce2c9853e2b Mon Sep 17 00:00:00 2001 From: Inferencebot Date: Wed, 7 Oct 2026 10:38:32 -0600 Subject: [PATCH] Provisioning: add new members to Open WebUI docs-RAG access group at signup --- app/main.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/app/main.py b/app/main.py index 459dfd9..6509514 100644 --- a/app/main.py +++ b/app/main.py @@ -72,6 +72,15 @@ PORTAL_SECRET = os.environ.get("PORTAL_SECRET", "") # email itself is tamper-proof. OWUI_JWT_SECRET = os.environ.get("OWUI_JWT_SECRET", "") +# Open WebUI admin API key — used at provisioning time to add the new member to +# Open WebUI's `members` group, which gates read access to the shared +# "Inference Cooperative Docs" RAG collection. Without this, a new member's +# first chat session has no docs grounding (the nightly refresh would only fix +# it hours later). Machine-to-machine only; never logged. +OWUI_API_KEY = os.environ.get("OWUI_API_KEY", "") +OWUI_BASE = os.environ.get("OWUI_BASE", "http://chat:8080") # internal Cloudron network +OWUI_MEMBERS_GROUP_ID = os.environ.get("OWUI_MEMBERS_GROUP_ID", "eb348e1a-059a-4a8f-8920-0f73942f6272") + # Shared secret that the member dashboard uses to authenticate broker requests. # The dashboard is Cloudron-SSO-gated and passes the authenticated member's # email; the portal trusts that email only because it carries this secret. Kept @@ -1209,6 +1218,64 @@ async def cloudron_set_active(user_id: str, active: bool) -> None: r.raise_for_status() +async def owui_add_to_members_group(cloudron_user_id: str) -> bool: + """Add a (new) member to Open WebUI's `members` group by their Cloudron uid. + + Open WebUI authenticates via Cloudron SSO and stores users keyed by the SSO + `sub` — which is the Cloudron user id (uid-…). The shared docs-RAG knowledge + collection is granted read to the `members` group, so group membership is + what makes the docs visible. Called at provisioning time so a new member's + FIRST chat session already has the docs (the nightly refresh sync is too + slow for that). + + Best-effort: any failure logs a warning and returns False — provisioning + must not fail over this; the nightly refresh self-heals within a day. + """ + if not OWUI_API_KEY: + logger.warning("OWUI_API_KEY not set; skipping Open WebUI group add for %s", cloudron_user_id) + return False + try: + async with httpx.AsyncClient(timeout=15.0) as client: + # find the OWUI user (SSO `sub` = Cloudron uid); if they've never + # logged in, the user row may not exist yet — the nightly sync + # covers it, and group-add by uid also works after first login. + r = await client.get( + f"{OWUI_BASE}/api/v1/users/", + headers={"Authorization": f"Bearer {OWUI_API_KEY}"}, + params={"query": cloudron_user_id}, + ) + if r.status_code != 200: + logger.warning("OWUI user lookup failed: %s %s", r.status_code, r.text[:150]) + return False + payload = r.json() or {} + users = payload.get("users", []) if isinstance(payload, dict) else (payload or []) + owui_uid = None + for u in users: + if u.get("id") == cloudron_user_id: + owui_uid = u["id"] + break + if not owui_uid: + # user hasn't logged in yet — they'll be added to the group by + # the nightly sync after first login. Not an error. + logger.info("OWUI: no user row yet for %s (not logged in); nightly sync will add", cloudron_user_id) + return False + + r = await client.post( + f"{OWUI_BASE}/api/v1/groups/id/{OWUI_MEMBERS_GROUP_ID}/users/add", + headers={"Authorization": f"Bearer {OWUI_API_KEY}"}, + json={"user_ids": [owui_uid]}, + ) + if r.status_code == 200: + logger.info("OWUI: added %s to members group", cloudron_user_id) + return True + # 400 "already in group" is fine + logger.warning("OWUI group add for %s: %s %s", cloudron_user_id, r.status_code, r.text[:150]) + return False + except Exception as e: + logger.warning("OWUI group add failed for %s: %s", cloudron_user_id, e) + return False + + async def cloudron_update_profile(user_id: str, email: str, display_name: str, fallback_email: str = "") -> None: """Update a user's profile (display name, email) after creation. @@ -1279,6 +1346,15 @@ async def provision_member(email: str, name: str, slug: str = "") -> str: balance = get_member_balance(email) team_id = await litellm_get_or_create_team(email, balance) + # Pre-provision the member into Open WebUI's docs-RAG access group so their + # FIRST chat session has the co-op docs. Best-effort (most new members + # haven't logged in yet, so the OWUI user row doesn't exist — the nightly + # refresh catches them after first login). + try: + await owui_add_to_members_group(user_id) + except Exception as e: + logger.warning("OWUI members-group add skipped for %s: %s", email, e) + # Reuse an existing key if we already have one (the sk- value is stored in # our DB at creation time and is the authoritative token for this member's # chat key). Only mint a fresh key when there is none.