nazdridoy commited on
Commit
bc34cae
Β·
verified Β·
1 Parent(s): 8c7976b

refactor(security): delegate org access control to Space metadata

Browse files

- [config] Add `hf_oauth_authorized_org` to Space metadata (README.md:9-10)
- [docs] Update documentation for `hf_oauth_authorized_org` and security section (README.md:23-32, 48)
- [remove] Remove `requests` import (utils.py:6)
- [remove] Remove OAuth/Org Access Utilities section and functions (utils.py:301-370)
- [remove] Remove `check_org_access` and `format_access_denied_message` imports from handler files (chat_handler.py:18-19, image_handler.py:19-20, tts_handler.py:19-20, video_handler.py:17-18)
- [update] Replace `check_org_access` calls with `if not access_token` checks in handler functions (chat_handler.py:178-179, 218-219, image_handler.py:290-291, 319-320, tts_handler.py:169-170, video_handler.py:142-143)
- [update] Change access denial messages to "Access Required" across handlers (chat_handler.py:181, 221; image_handler.py:292, 321; tts_handler.py:171; video_handler.py:144)
- [refactor] Add `username = None` before access token checks in all handlers (chat_handler.py:177, 217; image_handler.py:289, 318; tts_handler.py:168; video_handler.py:141)

Files changed (6) hide show
  1. README.md +12 -2
  2. chat_handler.py +8 -12
  3. image_handler.py +8 -10
  4. tts_handler.py +4 -6
  5. utils.py +0 -64
  6. video_handler.py +3 -5
README.md CHANGED
@@ -7,6 +7,8 @@ sdk: gradio
7
  app_file: app.py
8
  pinned: false
9
  hf_oauth: true
 
 
10
  ---
11
 
12
  ## πŸš€ HF‑Inferoxy AI Hub
@@ -23,7 +25,15 @@ A focused, multi‑modal AI workspace. Chat, create images, transform images, ge
23
  Add Space secrets:
24
  - `PROXY_URL`: HF‑Inferoxy server URL (e.g., `https://proxy.example.com`)
25
  - `PROXY_KEY`: API key for your proxy
26
- - `ALLOWED_ORGS`: Comma/space‑separated org slugs allowed to use the Space
 
 
 
 
 
 
 
 
27
 
28
  The app reads these at runtime β€” no extra setup required.
29
 
@@ -47,7 +57,7 @@ The app reads these at runtime β€” no extra setup required.
47
  Compatible with providers configured in HF‑Inferoxy, including `auto` (default), `hf-inference`, `cerebras`, `cohere`, `groq`, `together`, `fal-ai`, `replicate`, `nebius`, `nscale`, and others.
48
 
49
  ### Security
50
- - HF OAuth validates account/org membership (no inference scope).
51
  - Inference uses proxy‑managed tokens. Secrets are Space secrets.
52
  - RBAC, rotation, and quarantine handled by HF‑Inferoxy.
53
 
 
7
  app_file: app.py
8
  pinned: false
9
  hf_oauth: true
10
+ hf_oauth_authorized_org:
11
+ - nazdev
12
  ---
13
 
14
  ## πŸš€ HF‑Inferoxy AI Hub
 
25
  Add Space secrets:
26
  - `PROXY_URL`: HF‑Inferoxy server URL (e.g., `https://proxy.example.com`)
27
  - `PROXY_KEY`: API key for your proxy
28
+
29
+ Org access control: instead of a custom `ALLOWED_ORGS` secret and runtime checks, configure org restrictions in README metadata using `hf_oauth_authorized_org` per HF Spaces OAuth docs. Example:
30
+
31
+ ```yaml
32
+ hf_oauth: true
33
+ hf_oauth_authorized_org:
34
+ - your-org-slug
35
+ - another-org
36
+ ```
37
 
38
  The app reads these at runtime β€” no extra setup required.
39
 
 
57
  Compatible with providers configured in HF‑Inferoxy, including `auto` (default), `hf-inference`, `cerebras`, `cohere`, `groq`, `together`, `fal-ai`, `replicate`, `nebius`, `nscale`, and others.
58
 
59
  ### Security
60
+ - HF OAuth validates account; org membership is enforced by Space metadata (`hf_oauth_authorized_org`).
61
  - Inference uses proxy‑managed tokens. Secrets are Space secrets.
62
  - RBAC, rotation, and quarantine handled by HF‑Inferoxy.
63
 
chat_handler.py CHANGED
@@ -15,8 +15,6 @@ from hf_token_utils import get_proxy_token, report_token_status
15
  from utils import (
16
  validate_proxy_key,
17
  format_error_message,
18
- check_org_access,
19
- format_access_denied_message,
20
  render_with_reasoning_toggle
21
  )
22
 
@@ -178,12 +176,11 @@ def handle_chat_submit(message, history, system_msg, model_name, provider, max_t
178
  yield history, ""
179
  return
180
 
181
- # Enforce org-based access control via HF OAuth token
182
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
183
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
184
- if not is_allowed:
185
- # Show access denied as assistant message
186
- assistant_response = format_access_denied_message(access_msg)
187
  current_history = history + [{"role": "assistant", "content": assistant_response}]
188
  yield current_history, ""
189
  return
@@ -218,12 +215,11 @@ def handle_chat_retry(history, system_msg, model_name, provider, max_tokens, tem
218
  Retry the assistant response for the selected message.
219
  Works with gr.Chatbot.retry() which provides retry_data.index for the message.
220
  """
221
- # Enforce org-based access control via HF OAuth token
222
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
223
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
224
- if not is_allowed:
225
- # Show access denied as assistant message
226
- assistant_response = format_access_denied_message(access_msg)
227
  current_history = (history or []) + [{"role": "assistant", "content": assistant_response}]
228
  yield current_history
229
  return
 
15
  from utils import (
16
  validate_proxy_key,
17
  format_error_message,
 
 
18
  render_with_reasoning_toggle
19
  )
20
 
 
176
  yield history, ""
177
  return
178
 
179
+ # Require sign-in: if no token present, prompt login
180
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
181
+ username = None
182
+ if not access_token:
183
+ assistant_response = format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
 
184
  current_history = history + [{"role": "assistant", "content": assistant_response}]
185
  yield current_history, ""
186
  return
 
215
  Retry the assistant response for the selected message.
216
  Works with gr.Chatbot.retry() which provides retry_data.index for the message.
217
  """
218
+ # Require sign-in: if no token present, prompt login
219
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
220
+ username = None
221
+ if not access_token:
222
+ assistant_response = format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
 
223
  current_history = (history or []) + [{"role": "assistant", "content": assistant_response}]
224
  yield current_history
225
  return
image_handler.py CHANGED
@@ -17,8 +17,6 @@ from utils import (
17
  validate_proxy_key,
18
  format_error_message,
19
  format_success_message,
20
- check_org_access,
21
- format_access_denied_message
22
  )
23
 
24
  # Timeout configuration for image generation
@@ -289,11 +287,11 @@ def handle_image_to_image_generation(input_image_val, prompt_val, model_val, pro
289
  if input_image_val is None:
290
  return None, format_error_message("Validation Error", "Please upload an input image")
291
 
292
- # Enforce org-based access control via HF OAuth token
293
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
294
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
295
- if not is_allowed:
296
- return None, format_access_denied_message(access_msg)
297
 
298
  # Generate image-to-image
299
  return generate_image_to_image(
@@ -318,11 +316,11 @@ def handle_image_generation(prompt_val, model_val, provider_val, negative_prompt
318
  if not is_valid:
319
  return None, format_error_message("Validation Error", error_msg)
320
 
321
- # Enforce org-based access control via HF OAuth token
322
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
323
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
324
- if not is_allowed:
325
- return None, format_access_denied_message(access_msg)
326
 
327
  # Generate image
328
  return generate_image(
 
17
  validate_proxy_key,
18
  format_error_message,
19
  format_success_message,
 
 
20
  )
21
 
22
  # Timeout configuration for image generation
 
287
  if input_image_val is None:
288
  return None, format_error_message("Validation Error", "Please upload an input image")
289
 
290
+ # Require sign-in via HF OAuth token
291
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
292
+ username = None
293
+ if not access_token:
294
+ return None, format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
295
 
296
  # Generate image-to-image
297
  return generate_image_to_image(
 
316
  if not is_valid:
317
  return None, format_error_message("Validation Error", error_msg)
318
 
319
+ # Require sign-in via HF OAuth token
320
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
321
+ username = None
322
+ if not access_token:
323
+ return None, format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
324
 
325
  # Generate image
326
  return generate_image(
tts_handler.py CHANGED
@@ -18,8 +18,6 @@ from utils import (
18
  format_error_message,
19
  format_success_message,
20
  TTS_MODEL_CONFIGS,
21
- check_org_access,
22
- format_access_denied_message
23
  )
24
 
25
  # Timeout configuration for TTS generation
@@ -169,11 +167,11 @@ def handle_text_to_speech_generation(text_val, model_val, provider_val, voice_va
169
  if len(text_val) > 5000:
170
  return None, format_error_message("Validation Error", "Text is too long. Please keep it under 5000 characters.")
171
 
172
- # Enforce org-based access control via HF OAuth token
173
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
174
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
175
- if not is_allowed:
176
- return None, format_access_denied_message(access_msg)
177
 
178
  # Generate speech
179
  return generate_text_to_speech(
 
18
  format_error_message,
19
  format_success_message,
20
  TTS_MODEL_CONFIGS,
 
 
21
  )
22
 
23
  # Timeout configuration for TTS generation
 
167
  if len(text_val) > 5000:
168
  return None, format_error_message("Validation Error", "Text is too long. Please keep it under 5000 characters.")
169
 
170
+ # Require sign-in via HF OAuth token
171
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
172
+ username = None
173
+ if not access_token:
174
+ return None, format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
175
 
176
  # Generate speech
177
  return generate_text_to_speech(
utils.py CHANGED
@@ -5,7 +5,6 @@ Contains configuration constants and helper functions.
5
 
6
  import os
7
  import re
8
- import requests
9
 
10
 
11
  # Configuration constants
@@ -301,69 +300,6 @@ def get_gradio_theme():
301
  return None
302
 
303
 
304
- # -----------------------------
305
- # OAuth / Org Access Utilities
306
- # -----------------------------
307
-
308
- def _parse_allowed_orgs() -> list[str]:
309
- """Parse comma/space separated ALLOWED_ORGS env var into a list of lowercase names."""
310
- raw = os.getenv("ALLOWED_ORGS", "").strip()
311
- if not raw:
312
- return []
313
- # support comma or whitespace separated
314
- parts = [p.strip().lower() for p in raw.replace("\n", ",").replace(" ", ",").split(",") if p.strip()]
315
- return list(dict.fromkeys(parts)) # dedupe while preserving order
316
-
317
-
318
- def fetch_hf_identity(access_token: str) -> tuple[bool, dict | None, str]:
319
- """
320
- Call whoami-v2 to get user identity and orgs.
321
- Returns (success, data, error_message).
322
- """
323
- if not access_token:
324
- return False, None, "Missing access token"
325
- try:
326
- resp = requests.get(
327
- "https://huggingface.co/api/whoami-v2",
328
- headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
329
- timeout=15,
330
- )
331
- if resp.status_code != 200:
332
- return False, None, f"HF whoami-v2 HTTP {resp.status_code}"
333
- return True, resp.json(), ""
334
- except requests.exceptions.RequestException as e:
335
- return False, None, f"HF whoami-v2 error: {str(e)}"
336
-
337
-
338
- def check_org_access(access_token: str) -> tuple[bool, str, str | None, list[str]]:
339
- """
340
- Validate that the logged-in user belongs to any of ALLOWED_ORGS.
341
- Returns (is_allowed, message, username, matched_orgs).
342
- """
343
- allowed_orgs = _parse_allowed_orgs()
344
- if not access_token:
345
- return False, "πŸ”’ Please log in with Hugging Face to continue.", None, []
346
- if not allowed_orgs:
347
- return False, "❌ Access denied: ALLOWED_ORGS is not configured in Space secrets.", None, []
348
-
349
- ok, data, err = fetch_hf_identity(access_token)
350
- if not ok or not data:
351
- return False, f"❌ Failed to verify identity: {err}", None, []
352
-
353
- username = data.get("name") or data.get("fullname") or data.get("id")
354
- org_objs = data.get("orgs", []) or []
355
- user_org_names = [str(org.get("name", "")).lower() for org in org_objs if org.get("name")]
356
- matched = sorted(list(set(user_org_names).intersection(set(allowed_orgs))))
357
- if matched:
358
- return True, f"βœ… Access granted for @{username} in org(s): {', '.join(matched)}", username, matched
359
- return False, f"🚫 Access denied for @{username}. Required org(s): {', '.join(allowed_orgs)}", username, []
360
-
361
-
362
- def format_access_denied_message(message: str) -> str:
363
- """Return a standardized access denied message for UI display."""
364
- return format_error_message("Access Denied", message)
365
-
366
-
367
  # -----------------------------
368
  # Reasoning (<think>) utilities
369
  # -----------------------------
 
5
 
6
  import os
7
  import re
 
8
 
9
 
10
  # Configuration constants
 
300
  return None
301
 
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  # -----------------------------
304
  # Reasoning (<think>) utilities
305
  # -----------------------------
video_handler.py CHANGED
@@ -16,8 +16,6 @@ from utils import (
16
  validate_proxy_key,
17
  format_error_message,
18
  format_success_message,
19
- check_org_access,
20
- format_access_denied_message,
21
  )
22
 
23
 
@@ -142,9 +140,9 @@ def handle_video_generation(prompt_val, model_val, provider_val, steps_val, guid
142
  return None, format_error_message("Validation Error", "Please enter a prompt for video generation")
143
 
144
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
145
- is_allowed, access_msg, username, _matched = check_org_access(access_token)
146
- if not is_allowed:
147
- return None, format_access_denied_message(access_msg)
148
 
149
  return generate_video(
150
  prompt=prompt_val.strip(),
 
16
  validate_proxy_key,
17
  format_error_message,
18
  format_success_message,
 
 
19
  )
20
 
21
 
 
140
  return None, format_error_message("Validation Error", "Please enter a prompt for video generation")
141
 
142
  access_token = getattr(hf_token, "token", None) if hf_token is not None else None
143
+ username = None
144
+ if not access_token:
145
+ return None, format_error_message("Access Required", "Please sign in with Hugging Face (sidebar Login button).")
146
 
147
  return generate_video(
148
  prompt=prompt_val.strip(),