Skip to content

Commit

Permalink
fine grained token for admin (#3067)
Browse files Browse the repository at this point in the history
* fine grained token for admin

* simpler

* style

* fix tests
  • Loading branch information
lhoestq authored Sep 24, 2024
1 parent c0e223c commit 195a8ee
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 6 deletions.
2 changes: 1 addition & 1 deletion services/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The service can be configured using environment variables. They are grouped by s

Set environment variables to configure the application (`ADMIN_` prefix):

- `ADMIN_HF_ORGANIZATION`: the huggingface organization from which the authenticated user must be part of in order to access the protected routes, eg. "huggingface". If empty, the authentication is disabled. Defaults to None.
- `ADMIN_HF_ORGANIZATION`: the huggingface organization from which the authenticated user must be part of in order to access the protected routes, eg. "huggingface". If empty, the authentication is disabled. Defaults to None. Authentication requires a fine-grained token with `repo.write` permission.
- `ADMIN_CACHE_REPORTS_NUM_RESULTS`: the number of results in /cache-reports/... endpoints. Defaults to `100`.
- `ADMIN_CACHE_REPORTS_WITH_CONTENT_NUM_RESULTS`: the number of results in /cache-reports-with-content/... endpoints. Defaults to `100`.
- `ADMIN_HF_TIMEOUT_SECONDS`: the timeout in seconds for the requests to the Hugging Face Hub. Defaults to `0.2` (200 ms).
Expand Down
19 changes: 17 additions & 2 deletions services/admin/src/admin/authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2022 The HuggingFace Authors.

from collections.abc import Sequence
from typing import Literal, Optional

import httpx
Expand All @@ -14,6 +15,7 @@ async def auth_check(
request: Optional[Request] = None,
organization: Optional[str] = None,
hf_timeout_seconds: Optional[float] = None,
require_fine_grained_permissions: Sequence[str] = ("repo.write",),
) -> Literal[True]:
"""check if the user is member of the organization
Expand All @@ -25,6 +27,8 @@ async def auth_check(
authorized.
hf_timeout_seconds (`float`, *optional*): the timeout in seconds for the HTTP request to the external authentication
service.
require_fine_grained_permissions (`Sequence[str]`): require a fine-grained token with certain permissions
for the organization, if organization is provided. Defaults to ("repo.write",).
Returns:
`Literal[True]`: the user is authorized
Expand All @@ -39,10 +43,21 @@ async def auth_check(
if response.status_code == 200:
try:
json = response.json()
if organization is None or organization in {org["name"] for org in json["orgs"]}:
if organization is None or (
organization in {org["name"] for org in json["orgs"]}
and json["auth"]["type"] == "access_token"
and "fineGrained" in json["auth"]["accessToken"]
and any(
set(permission for permission in scope["permissions"]) >= set(require_fine_grained_permissions)
for scope in json["auth"]["accessToken"]["fineGrained"]["scoped"]
if scope["entity"]["name"] == organization and scope["entity"]["type"] == "org"
)
):
return True
else:
raise ExternalAuthenticatedError("You are not member of the organization")
raise ExternalAuthenticatedError(
"Cannot access the route with the current credentials. Please retry with other authentication credentials."
)
except Exception as err:
raise ExternalAuthenticatedError(
"Cannot access the route with the current credentials. Please retry with other authentication"
Expand Down
4 changes: 2 additions & 2 deletions services/admin/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def test_external_auth_responses_without_request(
status: int, error: Optional[type[Exception]], httpx_mock: HTTPXMock
) -> None:
url = "https://auth.check"
body = '{"orgs": [{"name": "org1"}]}'
body = '{"orgs": [{"name": "org1"}], "auth": {"type": "access_token", "accessToken": {"fineGrained": {"scoped": [{"entity": {"type": "org", "name": "org1"}, "permissions": ["repo.write"]}]}}}}'
httpx_mock.add_response(method="GET", url=url, status_code=status, text=body)
if error is None:
assert await auth_check(external_auth_url=url, organization="org1")
Expand All @@ -55,7 +55,7 @@ async def test_external_auth_responses_without_request(
)
async def test_org(org: str, status: int, error: Optional[type[Exception]], httpx_mock: HTTPXMock) -> None:
url = "https://auth.check"
body = '{"orgs": [{"name": "org1"}]}'
body = '{"orgs": [{"name": "org1"}], "auth": {"type": "access_token", "accessToken": {"fineGrained": {"scoped": [{"entity": {"type": "org", "name": "org1"}, "permissions": ["repo.write"]}]}}}}'
httpx_mock.add_response(method="GET", url=url, status_code=status, text=body)
if error is None:
assert await auth_check(external_auth_url=url, organization=org)
Expand Down
2 changes: 1 addition & 1 deletion services/admin/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def request_callback(request: httpx.Request) -> httpx.Response:
# and 200 if none has been provided
# there is no logic behind this behavior, it's just to test if the
# tokens are correctly passed to the auth_check service
body = '{"orgs": [{"name": "org1"}]}'
body = '{"orgs": [{"name": "org1"}], "auth": {"type": "access_token", "accessToken": {"fineGrained": {"scoped": [{"entity": {"type": "org", "name": "org1"}, "permissions": ["repo.write"]}]}}}}'
if request.headers.get("authorization"):
return httpx.Response(status_code=404, text=body)
return httpx.Response(status_code=200, text=body)

0 comments on commit 195a8ee

Please sign in to comment.