Allow Space OAuth collection writes
Browse filesCo-authored-by: OpenAI Codex <codex@openai.com>
- README.md +1 -0
- backend/routes/auth.py +13 -1
- tests/unit/test_auth_token_propagation.py +14 -0
README.md
CHANGED
|
@@ -12,6 +12,7 @@ hf_oauth_scopes:
|
|
| 12 |
- write-repos
|
| 13 |
- contribute-repos
|
| 14 |
- manage-repos
|
|
|
|
| 15 |
- inference-api
|
| 16 |
- jobs
|
| 17 |
- write-discussions
|
|
|
|
| 12 |
- write-repos
|
| 13 |
- contribute-repos
|
| 14 |
- manage-repos
|
| 15 |
+
- write-collections
|
| 16 |
- inference-api
|
| 17 |
- jobs
|
| 18 |
- write-discussions
|
backend/routes/auth.py
CHANGED
|
@@ -20,6 +20,18 @@ router = APIRouter(prefix="/auth", tags=["auth"])
|
|
| 20 |
OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID", "")
|
| 21 |
OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
|
| 22 |
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# In-memory OAuth state store with expiry (5 min TTL)
|
| 25 |
_OAUTH_STATE_TTL = 300
|
|
@@ -69,7 +81,7 @@ async def oauth_login(request: Request) -> RedirectResponse:
|
|
| 69 |
params = {
|
| 70 |
"client_id": OAUTH_CLIENT_ID,
|
| 71 |
"redirect_uri": get_redirect_uri(request),
|
| 72 |
-
"scope": "
|
| 73 |
"response_type": "code",
|
| 74 |
"state": state,
|
| 75 |
}
|
|
|
|
| 20 |
OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID", "")
|
| 21 |
OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
|
| 22 |
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
|
| 23 |
+
OAUTH_SCOPES = (
|
| 24 |
+
"openid",
|
| 25 |
+
"profile",
|
| 26 |
+
"read-repos",
|
| 27 |
+
"write-repos",
|
| 28 |
+
"contribute-repos",
|
| 29 |
+
"manage-repos",
|
| 30 |
+
"write-collections",
|
| 31 |
+
"inference-api",
|
| 32 |
+
"jobs",
|
| 33 |
+
"write-discussions",
|
| 34 |
+
)
|
| 35 |
|
| 36 |
# In-memory OAuth state store with expiry (5 min TTL)
|
| 37 |
_OAUTH_STATE_TTL = 300
|
|
|
|
| 81 |
params = {
|
| 82 |
"client_id": OAUTH_CLIENT_ID,
|
| 83 |
"redirect_uri": get_redirect_uri(request),
|
| 84 |
+
"scope": " ".join(OAUTH_SCOPES),
|
| 85 |
"response_type": "code",
|
| 86 |
"state": state,
|
| 87 |
}
|
tests/unit/test_auth_token_propagation.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
import sys
|
| 4 |
from pathlib import Path
|
| 5 |
from types import SimpleNamespace
|
|
|
|
| 6 |
|
| 7 |
import pytest
|
| 8 |
|
|
@@ -59,3 +60,16 @@ async def test_auth_me_does_not_expose_internal_hf_token():
|
|
| 59 |
"username": "alice",
|
| 60 |
"authenticated": True,
|
| 61 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import sys
|
| 4 |
from pathlib import Path
|
| 5 |
from types import SimpleNamespace
|
| 6 |
+
from urllib.parse import parse_qs, urlparse
|
| 7 |
|
| 8 |
import pytest
|
| 9 |
|
|
|
|
| 60 |
"username": "alice",
|
| 61 |
"authenticated": True,
|
| 62 |
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@pytest.mark.asyncio
|
| 66 |
+
async def test_oauth_login_requests_collection_write_scope(monkeypatch):
|
| 67 |
+
monkeypatch.setattr(auth, "OAUTH_CLIENT_ID", "oauth-client")
|
| 68 |
+
monkeypatch.setenv("SPACE_HOST", "example.hf.space")
|
| 69 |
+
auth.oauth_states.clear()
|
| 70 |
+
|
| 71 |
+
response = await auth.oauth_login(SimpleNamespace())
|
| 72 |
+
params = parse_qs(urlparse(response.headers["location"]).query)
|
| 73 |
+
scopes = set(params["scope"][0].split())
|
| 74 |
+
|
| 75 |
+
assert "write-collections" in scopes
|