andito HF Staff commited on
Commit
c48887e
·
1 Parent(s): 195cac9

Deploy PRO tier resolution fix

Browse files
Files changed (2) hide show
  1. auth.py +35 -22
  2. server.py +1 -1
auth.py CHANGED
@@ -88,9 +88,9 @@ def _field(obj, name, default=None):
88
  # verifying org gating on the live Space without guessing.
89
  AUTH_DEBUG = bool(os.environ.get("AUTH_DEBUG"))
90
 
91
- # whoami-v2 org lookups are cached for the process lifetime, keyed by token, so
92
- # /api/me + /api/session don't each hit the Hub.
93
- _orgs_cache: "dict[str, set[str]]" = {}
94
 
95
 
96
  def current_oauth(request):
@@ -160,14 +160,12 @@ def _user_org_names(user) -> "set[str]":
160
  return names
161
 
162
 
163
- def _orgs_via_token(token: str) -> "set[str]":
164
- """Fallback org lookup via the Hub `whoami-v2` API, using the user's OAuth
165
- access token. Covers the case where the userinfo claim omits `orgs`."""
166
  if not token:
167
- return set()
168
- if token in _orgs_cache:
169
- return _orgs_cache[token]
170
- names: "set[str]" = set()
171
  try:
172
  import httpx
173
 
@@ -177,24 +175,36 @@ def _orgs_via_token(token: str) -> "set[str]":
177
  timeout=5.0,
178
  )
179
  resp.raise_for_status()
180
- for org in resp.json().get("orgs", []) or []:
181
- for key in ("name", "fullname"):
182
- val = org.get(key)
183
- if val:
184
- names.add(str(val).lower())
185
  except Exception as exc: # pragma: no cover - network/permission dependent
186
- logger.info("whoami-v2 org lookup failed: %r", exc)
187
- _orgs_cache[token] = names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  return names
189
 
190
 
191
- def _org_names(user, token=None, allow=None) -> "set[str]":
192
  """The user's org usernames from the OAuth userinfo claim. If that doesn't
193
  already satisfy `allow`, fall back to the Hub `whoami-v2` API (the claim is
194
  often empty or partial), so membership is resolved either way."""
195
  names = _user_org_names(user)
196
  if token and (allow is None or not (allow & names)):
197
- names = names | _orgs_via_token(token)
198
  return names
199
 
200
 
@@ -203,15 +213,18 @@ def resolve_tier(user, token=None) -> str:
203
  member, unlimited), or 'free'. PRO wins over org if both apply."""
204
  if bool(_field(user, "is_pro", False)):
205
  return "pro"
 
 
 
206
  allow = _unlimited_orgs()
207
- names = _org_names(user, token, allow)
208
  tier = "org" if (allow & names) else "free"
209
  if AUTH_DEBUG:
210
  logger.info("tier=%s orgs=%s allow=%s", tier, sorted(names), sorted(allow))
211
  return tier
212
 
213
 
214
- def user_view(request) -> dict:
215
  """Public profile for /api/me."""
216
  info = current_oauth(request)
217
  user = _field(info, "user_info")
@@ -226,7 +239,7 @@ def user_view(request) -> dict:
226
  "loggedIn": True,
227
  "username": _field(user, "preferred_username") or _field(user, "name") or "you",
228
  "avatar": _field(user, "picture"),
229
- "tier": resolve_tier(user, token),
230
  }
231
  if AUTH_DEBUG:
232
  out["orgs"] = sorted(_org_names(user, token))
 
88
  # verifying org gating on the live Space without guessing.
89
  AUTH_DEBUG = bool(os.environ.get("AUTH_DEBUG"))
90
 
91
+ # whoami-v2 profiles are cached for the process lifetime, keyed by token, so
92
+ # tier and org resolution across /api/me + /api/session share one Hub request.
93
+ _whoami_cache: "dict[str, dict]" = {}
94
 
95
 
96
  def current_oauth(request):
 
160
  return names
161
 
162
 
163
+ def _whoami_via_token(token: str) -> dict:
164
+ """The authenticated Hub `whoami-v2` profile, cached by OAuth token."""
 
165
  if not token:
166
+ return {}
167
+ if token in _whoami_cache:
168
+ return _whoami_cache[token]
 
169
  try:
170
  import httpx
171
 
 
175
  timeout=5.0,
176
  )
177
  resp.raise_for_status()
178
+ data = resp.json()
179
+ if not isinstance(data, dict):
180
+ raise ValueError("whoami-v2 returned a non-object response")
 
 
181
  except Exception as exc: # pragma: no cover - network/permission dependent
182
+ logger.info("whoami-v2 profile lookup failed: %r", exc)
183
+ return {}
184
+ _whoami_cache[token] = data
185
+ return data
186
+
187
+
188
+ def _orgs_via_token(token: str, profile=None) -> "set[str]":
189
+ """Org names from the cached authenticated Hub profile."""
190
+ if profile is None:
191
+ profile = _whoami_via_token(token)
192
+ names: "set[str]" = set()
193
+ for org in profile.get("orgs", []) or []:
194
+ for key in ("name", "fullname"):
195
+ val = _field(org, key)
196
+ if val:
197
+ names.add(str(val).lower())
198
  return names
199
 
200
 
201
+ def _org_names(user, token=None, allow=None, profile=None) -> "set[str]":
202
  """The user's org usernames from the OAuth userinfo claim. If that doesn't
203
  already satisfy `allow`, fall back to the Hub `whoami-v2` API (the claim is
204
  often empty or partial), so membership is resolved either way."""
205
  names = _user_org_names(user)
206
  if token and (allow is None or not (allow & names)):
207
+ names = names | _orgs_via_token(token, profile)
208
  return names
209
 
210
 
 
213
  member, unlimited), or 'free'. PRO wins over org if both apply."""
214
  if bool(_field(user, "is_pro", False)):
215
  return "pro"
216
+ profile = _whoami_via_token(token)
217
+ if bool(profile.get("isPro", False)):
218
+ return "pro"
219
  allow = _unlimited_orgs()
220
+ names = _org_names(user, token, allow, profile)
221
  tier = "org" if (allow & names) else "free"
222
  if AUTH_DEBUG:
223
  logger.info("tier=%s orgs=%s allow=%s", tier, sorted(names), sorted(allow))
224
  return tier
225
 
226
 
227
+ def user_view(request, tier=None) -> dict:
228
  """Public profile for /api/me."""
229
  info = current_oauth(request)
230
  user = _field(info, "user_info")
 
239
  "loggedIn": True,
240
  "username": _field(user, "preferred_username") or _field(user, "name") or "you",
241
  "avatar": _field(user, "picture"),
242
+ "tier": tier if tier is not None else resolve_tier(user, token),
243
  }
244
  if AUTH_DEBUG:
245
  out["orgs"] = sorted(_org_names(user, token))
server.py CHANGED
@@ -153,8 +153,8 @@ async def me(request: Request):
153
  sets the anonymous tracking cookie when first seen."""
154
  if not LIMITER_ENABLED:
155
  return {"enabled": False}
156
- view = auth.user_view(request)
157
  tier, keys, set_cookie = auth.resolve_identity(request)
 
158
  unlimited = limiter.budget_for(tier) is None
159
  rem = None if unlimited else await asyncio.to_thread(limiter.remaining, keys, tier)
160
  out = {
 
153
  sets the anonymous tracking cookie when first seen."""
154
  if not LIMITER_ENABLED:
155
  return {"enabled": False}
 
156
  tier, keys, set_cookie = auth.resolve_identity(request)
157
+ view = auth.user_view(request, tier=tier)
158
  unlimited = limiter.budget_for(tier) is None
159
  rem = None if unlimited else await asyncio.to_thread(limiter.remaining, keys, tier)
160
  out = {