fix: stop using request.username on HF Spaces (returns Space owner, not visitor)
Browse filesOn HF Spaces, request.username returns the Space owner's username for
all visitors. This caused every user to appear as "raylim" regardless
of who was actually logged in. Now only OAuthProfile from
gr.LoginButton() is used to identify users on HF Spaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- src/mosaic/telemetry/utils.py +9 -31
- src/mosaic/ui/user_tabs.py +7 -12
- tests/telemetry/test_utils.py +10 -10
- tests/test_ui_user_storage.py +13 -2
src/mosaic/telemetry/utils.py
CHANGED
|
@@ -113,16 +113,17 @@ class UserInfo:
|
|
| 113 |
|
| 114 |
|
| 115 |
def extract_user_info(request, is_hf_spaces: bool = False, profile=None) -> UserInfo:
|
| 116 |
-
"""Extract user info from Gradio OAuth profile
|
| 117 |
|
| 118 |
With gr.LoginButton(), Gradio provides user info via gr.OAuthProfile
|
| 119 |
(injected automatically into event handlers). The profile object has
|
| 120 |
a `username` attribute with the HuggingFace username.
|
| 121 |
|
| 122 |
-
|
|
|
|
| 123 |
|
| 124 |
Args:
|
| 125 |
-
request: Gradio request object (
|
| 126 |
is_hf_spaces: Whether running on HuggingFace Spaces (only extract on HF)
|
| 127 |
profile: Gradio OAuthProfile object (injected by LoginButton OAuth flow)
|
| 128 |
|
|
@@ -137,7 +138,9 @@ def extract_user_info(request, is_hf_spaces: bool = False, profile=None) -> User
|
|
| 137 |
if not is_hf_spaces:
|
| 138 |
return UserInfo()
|
| 139 |
|
| 140 |
-
#
|
|
|
|
|
|
|
| 141 |
if profile is not None:
|
| 142 |
try:
|
| 143 |
username = getattr(profile, "username", None)
|
|
@@ -147,30 +150,5 @@ def extract_user_info(request, is_hf_spaces: bool = False, profile=None) -> User
|
|
| 147 |
except Exception as e:
|
| 148 |
logger.debug(f"Could not extract username from OAuthProfile: {e}")
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
logger.debug(
|
| 153 |
-
"Cannot extract user info: request and profile are both None/empty"
|
| 154 |
-
)
|
| 155 |
-
return UserInfo()
|
| 156 |
-
|
| 157 |
-
try:
|
| 158 |
-
username = None
|
| 159 |
-
|
| 160 |
-
try:
|
| 161 |
-
username = request.username
|
| 162 |
-
except Exception as auth_error:
|
| 163 |
-
logger.debug(f"Could not access request.username: {auth_error}")
|
| 164 |
-
|
| 165 |
-
if username:
|
| 166 |
-
logger.info(f"Extracted user from request.username: {username}")
|
| 167 |
-
return UserInfo(is_logged_in=True, username=username)
|
| 168 |
-
else:
|
| 169 |
-
logger.debug(
|
| 170 |
-
"User info not available: no OAuthProfile and request.username is None"
|
| 171 |
-
)
|
| 172 |
-
return UserInfo()
|
| 173 |
-
|
| 174 |
-
except Exception as e:
|
| 175 |
-
logger.error(f"Unexpected error extracting user info: {e}")
|
| 176 |
-
return UserInfo()
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
def extract_user_info(request, is_hf_spaces: bool = False, profile=None) -> UserInfo:
|
| 116 |
+
"""Extract user info from Gradio OAuth profile.
|
| 117 |
|
| 118 |
With gr.LoginButton(), Gradio provides user info via gr.OAuthProfile
|
| 119 |
(injected automatically into event handlers). The profile object has
|
| 120 |
a `username` attribute with the HuggingFace username.
|
| 121 |
|
| 122 |
+
Does NOT use request.username, which on HF Spaces returns the Space
|
| 123 |
+
owner's username rather than the visitor's.
|
| 124 |
|
| 125 |
Args:
|
| 126 |
+
request: Gradio request object (unused, kept for API compatibility)
|
| 127 |
is_hf_spaces: Whether running on HuggingFace Spaces (only extract on HF)
|
| 128 |
profile: Gradio OAuthProfile object (injected by LoginButton OAuth flow)
|
| 129 |
|
|
|
|
| 138 |
if not is_hf_spaces:
|
| 139 |
return UserInfo()
|
| 140 |
|
| 141 |
+
# On HF Spaces, only OAuthProfile reliably identifies the logged-in user.
|
| 142 |
+
# request.username returns the Space owner's username (not the visitor's),
|
| 143 |
+
# so we must NOT fall back to it.
|
| 144 |
if profile is not None:
|
| 145 |
try:
|
| 146 |
username = getattr(profile, "username", None)
|
|
|
|
| 150 |
except Exception as e:
|
| 151 |
logger.debug(f"Could not extract username from OAuthProfile: {e}")
|
| 152 |
|
| 153 |
+
logger.debug("User not logged in: no OAuthProfile available")
|
| 154 |
+
return UserInfo()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/mosaic/ui/user_tabs.py
CHANGED
|
@@ -32,18 +32,20 @@ LOCAL_DEBUG_USERNAME = "local_user"
|
|
| 32 |
def _get_username(request: gr.Request = None, profile=None) -> tuple[str, bool]:
|
| 33 |
"""Get username for storage operations.
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
Returns:
|
| 40 |
Tuple of (username, is_local_mode)
|
| 41 |
-
- In HF Spaces: (profile.username
|
| 42 |
- Locally: (LOCAL_DEBUG_USERNAME, True)
|
| 43 |
- HF Spaces not logged in: (None, False)
|
| 44 |
"""
|
| 45 |
if IS_HF_SPACES:
|
| 46 |
-
#
|
|
|
|
|
|
|
| 47 |
if profile is not None:
|
| 48 |
try:
|
| 49 |
username = getattr(profile, "username", None)
|
|
@@ -52,13 +54,6 @@ def _get_username(request: gr.Request = None, profile=None) -> tuple[str, bool]:
|
|
| 52 |
except Exception:
|
| 53 |
pass
|
| 54 |
|
| 55 |
-
# Fallback: request.username
|
| 56 |
-
try:
|
| 57 |
-
if request and request.username:
|
| 58 |
-
return (request.username, False)
|
| 59 |
-
except Exception:
|
| 60 |
-
pass
|
| 61 |
-
|
| 62 |
return (None, False)
|
| 63 |
else:
|
| 64 |
# Local mode - use debug username
|
|
|
|
| 32 |
def _get_username(request: gr.Request = None, profile=None) -> tuple[str, bool]:
|
| 33 |
"""Get username for storage operations.
|
| 34 |
|
| 35 |
+
On HF Spaces, uses OAuthProfile from gr.LoginButton() to identify
|
| 36 |
+
the logged-in user. Does NOT use request.username, which returns
|
| 37 |
+
the Space owner's username rather than the visitor's.
|
| 38 |
|
| 39 |
Returns:
|
| 40 |
Tuple of (username, is_local_mode)
|
| 41 |
+
- In HF Spaces: (profile.username, False) if logged in
|
| 42 |
- Locally: (LOCAL_DEBUG_USERNAME, True)
|
| 43 |
- HF Spaces not logged in: (None, False)
|
| 44 |
"""
|
| 45 |
if IS_HF_SPACES:
|
| 46 |
+
# On HF Spaces, only OAuthProfile reliably identifies the logged-in user.
|
| 47 |
+
# request.username returns the Space owner's username (not the visitor's),
|
| 48 |
+
# so we must NOT fall back to it.
|
| 49 |
if profile is not None:
|
| 50 |
try:
|
| 51 |
username = getattr(profile, "username", None)
|
|
|
|
| 54 |
except Exception:
|
| 55 |
pass
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
return (None, False)
|
| 58 |
else:
|
| 59 |
# Local mode - use debug username
|
tests/telemetry/test_utils.py
CHANGED
|
@@ -182,9 +182,9 @@ class TestExtractUserInfo:
|
|
| 182 |
return MockRequest(username)
|
| 183 |
|
| 184 |
def test_extract_user_info_with_logged_in_user(self):
|
| 185 |
-
"""Test extraction with a logged-in user."""
|
| 186 |
-
|
| 187 |
-
user_info = extract_user_info(
|
| 188 |
|
| 189 |
assert user_info.is_logged_in is True
|
| 190 |
assert user_info.username == "testuser123"
|
|
@@ -229,9 +229,9 @@ class TestExtractUserInfo:
|
|
| 229 |
|
| 230 |
def test_extract_user_info_with_special_characters(self):
|
| 231 |
"""Test extraction with username containing special characters."""
|
| 232 |
-
|
| 233 |
|
| 234 |
-
user_info = extract_user_info(
|
| 235 |
|
| 236 |
assert user_info.is_logged_in is True
|
| 237 |
assert user_info.username == "user-name_123"
|
|
@@ -274,14 +274,14 @@ class TestExtractUserInfo:
|
|
| 274 |
assert user_info.is_logged_in is True
|
| 275 |
assert user_info.username == "oauth_user"
|
| 276 |
|
| 277 |
-
def
|
| 278 |
-
"""Test
|
| 279 |
-
request = self._create_mock_request("
|
| 280 |
|
| 281 |
user_info = extract_user_info(request, is_hf_spaces=True, profile=None)
|
| 282 |
|
| 283 |
-
assert user_info.is_logged_in is
|
| 284 |
-
assert user_info.username
|
| 285 |
|
| 286 |
def test_extract_user_info_no_profile_not_hf_spaces(self):
|
| 287 |
"""Test that profile is ignored when not on HF Spaces."""
|
|
|
|
| 182 |
return MockRequest(username)
|
| 183 |
|
| 184 |
def test_extract_user_info_with_logged_in_user(self):
|
| 185 |
+
"""Test extraction with a logged-in user via OAuthProfile."""
|
| 186 |
+
profile = self._create_mock_profile("testuser123")
|
| 187 |
+
user_info = extract_user_info(None, is_hf_spaces=True, profile=profile)
|
| 188 |
|
| 189 |
assert user_info.is_logged_in is True
|
| 190 |
assert user_info.username == "testuser123"
|
|
|
|
| 229 |
|
| 230 |
def test_extract_user_info_with_special_characters(self):
|
| 231 |
"""Test extraction with username containing special characters."""
|
| 232 |
+
profile = self._create_mock_profile("user-name_123")
|
| 233 |
|
| 234 |
+
user_info = extract_user_info(None, is_hf_spaces=True, profile=profile)
|
| 235 |
|
| 236 |
assert user_info.is_logged_in is True
|
| 237 |
assert user_info.username == "user-name_123"
|
|
|
|
| 274 |
assert user_info.is_logged_in is True
|
| 275 |
assert user_info.username == "oauth_user"
|
| 276 |
|
| 277 |
+
def test_extract_user_info_ignores_request_username(self):
|
| 278 |
+
"""Test that request.username is NOT used (it returns Space owner, not visitor)."""
|
| 279 |
+
request = self._create_mock_request("space_owner")
|
| 280 |
|
| 281 |
user_info = extract_user_info(request, is_hf_spaces=True, profile=None)
|
| 282 |
|
| 283 |
+
assert user_info.is_logged_in is False
|
| 284 |
+
assert user_info.username is None
|
| 285 |
|
| 286 |
def test_extract_user_info_no_profile_not_hf_spaces(self):
|
| 287 |
"""Test that profile is ignored when not on HF Spaces."""
|
tests/test_ui_user_storage.py
CHANGED
|
@@ -86,11 +86,22 @@ class TestUsernameExtraction:
|
|
| 86 |
|
| 87 |
@patch("mosaic.ui.user_tabs.IS_HF_SPACES", True)
|
| 88 |
def test_hf_logged_in_returns_username(self, mock_request_hf_logged_in):
|
| 89 |
-
"""HF Spaces logged in should return
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
assert username == "test_user"
|
| 92 |
assert is_local is False
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
@patch("mosaic.ui.user_tabs.IS_HF_SPACES", True)
|
| 95 |
def test_hf_not_logged_in_returns_none(self, mock_request_hf_not_logged_in):
|
| 96 |
"""HF Spaces not logged in should return None."""
|
|
|
|
| 86 |
|
| 87 |
@patch("mosaic.ui.user_tabs.IS_HF_SPACES", True)
|
| 88 |
def test_hf_logged_in_returns_username(self, mock_request_hf_logged_in):
|
| 89 |
+
"""HF Spaces logged in should return OAuthProfile username, not request.username."""
|
| 90 |
+
|
| 91 |
+
class MockProfile:
|
| 92 |
+
username = "test_user"
|
| 93 |
+
|
| 94 |
+
username, is_local = _get_username(mock_request_hf_logged_in, profile=MockProfile())
|
| 95 |
assert username == "test_user"
|
| 96 |
assert is_local is False
|
| 97 |
|
| 98 |
+
@patch("mosaic.ui.user_tabs.IS_HF_SPACES", True)
|
| 99 |
+
def test_hf_request_username_ignored(self, mock_request_hf_logged_in):
|
| 100 |
+
"""HF Spaces should NOT use request.username (returns Space owner)."""
|
| 101 |
+
username, is_local = _get_username(mock_request_hf_logged_in, profile=None)
|
| 102 |
+
assert username is None
|
| 103 |
+
assert is_local is False
|
| 104 |
+
|
| 105 |
@patch("mosaic.ui.user_tabs.IS_HF_SPACES", True)
|
| 106 |
def test_hf_not_logged_in_returns_none(self, mock_request_hf_not_logged_in):
|
| 107 |
"""HF Spaces not logged in should return None."""
|