Skip to content

Commit a12793b

Browse files
authored
fix: Remove unused scope-based endpoint authorization feature (llamastack#4734)
The scope-based authorization feature was only used by the telemetry API endpoints which were removed in commit 866c13c. Since no endpoints currently use this feature, remove it entirely. Changes: o Remove required_scope parameter from WebMethod dataclass and webmethod decorator o Remove scope checking logic from AuthenticationMiddleware o Remove _has_required_scope helper function o Remove scope authorization documentation o Remove scope-related tests and fixtures -- This code is unused since llamastack#3740
1 parent 4f49445 commit a12793b

File tree

4 files changed

+2
-209
lines changed

4 files changed

+2
-209
lines changed

docs/docs/distributions/configuration.mdx

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -794,36 +794,6 @@ For a request to succeed:
794794
2. User must pass resource authorization (can read `model::llama-3-2-3b`)
795795
3. Both checks must pass
796796

797-
#### API Endpoint Authorization with Scopes
798-
799-
In addition to resource-based access control, Llama Stack supports endpoint-level authorization using OAuth 2.0 style scopes. When authentication is enabled, specific API endpoints require users to have particular scopes in their authentication token.
800-
801-
**Authentication Configuration:**
802-
803-
For **JWT/OAuth2 providers**, scopes should be included in the JWT's claims:
804-
```json
805-
{
806-
"sub": "user123",
807-
"scope": "<scope>",
808-
"aud": "llama-stack"
809-
}
810-
```
811-
812-
For **custom authentication providers**, the endpoint must return user attributes including the `scopes` array:
813-
```json
814-
{
815-
"principal": "user123",
816-
"attributes": {
817-
"scopes": ["<scope>"]
818-
}
819-
}
820-
```
821-
822-
**Behavior:**
823-
- Users without the required scope receive a 403 Forbidden response
824-
- When authentication is disabled, scope checks are bypassed
825-
- Endpoints without `required_scope` work normally for all authenticated users
826-
827797
### Quota Configuration
828798

829799
The `quota` section allows you to enable server-side request throttling for both

src/llama_stack/core/server/auth.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,6 @@ async def __call__(self, scope, receive, send):
154154
f"Authentication successful: {validation_result.principal} with {len(validation_result.attributes)} attributes"
155155
)
156156

157-
# Scope-based API access control
158-
if webmethod and webmethod.required_scope:
159-
user = user_from_scope(scope)
160-
if not _has_required_scope(webmethod.required_scope, user):
161-
return await self._send_auth_error(
162-
send,
163-
f"Access denied: user does not have required scope: {webmethod.required_scope}",
164-
status=403,
165-
)
166-
167157
return await self.app(scope, receive, send)
168158

169159
async def _send_auth_error(self, send, message, status=401):
@@ -179,18 +169,6 @@ async def _send_auth_error(self, send, message, status=401):
179169
await send({"type": "http.response.body", "body": error_msg})
180170

181171

182-
def _has_required_scope(required_scope: str, user: User | None) -> bool:
183-
# if no user, assume auth is not enabled
184-
if not user:
185-
return True
186-
187-
if not user.attributes:
188-
return False
189-
190-
user_scopes = user.attributes.get("scopes", [])
191-
return required_scope in user_scopes
192-
193-
194172
class RouteAuthorizationMiddleware:
195173
"""Middleware that enforces route-level access control.
196174

src/llama_stack_api/schema_utils.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ class WebMethod:
149149
raw_bytes_request_body: bool | None = False
150150
# A descriptive name of the corresponding span created by tracing
151151
descriptive_name: str | None = None
152-
required_scope: str | None = None
153152
deprecated: bool | None = False
154153
require_authentication: bool | None = True
155154

@@ -166,7 +165,6 @@ def webmethod(
166165
response_examples: list[Any] | None = None,
167166
raw_bytes_request_body: bool | None = False,
168167
descriptive_name: str | None = None,
169-
required_scope: str | None = None,
170168
deprecated: bool | None = False,
171169
require_authentication: bool | None = True,
172170
) -> Callable[[CallableT], CallableT]:
@@ -177,7 +175,6 @@ def webmethod(
177175
:param public: True if the operation can be invoked without prior authentication.
178176
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
179177
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
180-
:param required_scope: Required scope for this endpoint (e.g., 'monitoring.viewer').
181178
:param require_authentication: Whether this endpoint requires authentication (default True).
182179
"""
183180

@@ -191,7 +188,6 @@ def wrap(func: CallableT) -> CallableT:
191188
response_examples=response_examples,
192189
raw_bytes_request_body=raw_bytes_request_body,
193190
descriptive_name=descriptive_name,
194-
required_scope=required_scope,
195191
deprecated=deprecated,
196192
require_authentication=require_authentication if require_authentication is not None else True,
197193
)

tests/unit/server/test_auth.py

Lines changed: 2 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
OAuth2JWKSConfig,
2222
OAuth2TokenAuthConfig,
2323
)
24-
from llama_stack.core.request_headers import User
25-
from llama_stack.core.server.auth import AuthenticationMiddleware, _has_required_scope
24+
from llama_stack.core.server.auth import AuthenticationMiddleware
2625
from llama_stack.core.server.auth_providers import (
2726
get_attributes_from_claims,
2827
)
@@ -143,11 +142,10 @@ def middleware_with_mocks(mock_auth_endpoint):
143142
)
144143
middleware = AuthenticationMiddleware(mock_app, auth_config, {})
145144

146-
# Mock the route_impls to simulate finding routes with required scopes
147145
from llama_stack_api import WebMethod
148146

149147
routes = {
150-
("POST", "/test/scoped"): WebMethod(route="/test/scoped", method="POST", required_scope="test.read"),
148+
("POST", "/test/scoped"): WebMethod(route="/test/scoped", method="POST"),
151149
("GET", "/test/public"): WebMethod(route="/test/public", method="GET"),
152150
("GET", "/health"): WebMethod(route="/health", method="GET", require_authentication=False),
153151
("GET", "/version"): WebMethod(route="/version", method="GET", require_authentication=False),
@@ -193,36 +191,6 @@ async def mock_post_exception(*args, **kwargs):
193191
raise Exception("Connection error")
194192

195193

196-
async def mock_post_success_with_scope(*args, **kwargs):
197-
"""Mock auth response for user with test.read scope"""
198-
return MockResponse(
199-
200,
200-
{
201-
"message": "Authentication successful",
202-
"principal": "test-user",
203-
"attributes": {
204-
"scopes": ["test.read", "other.scope"],
205-
"roles": ["user"],
206-
},
207-
},
208-
)
209-
210-
211-
async def mock_post_success_no_scope(*args, **kwargs):
212-
"""Mock auth response for user without required scope"""
213-
return MockResponse(
214-
200,
215-
{
216-
"message": "Authentication successful",
217-
"principal": "test-user",
218-
"attributes": {
219-
"scopes": ["other.scope"],
220-
"roles": ["user"],
221-
},
222-
},
223-
)
224-
225-
226194
# HTTP Endpoint Tests
227195
def test_missing_auth_header(http_client):
228196
response = http_client.get("/test")
@@ -781,125 +749,6 @@ def test_valid_introspection_with_custom_mapping_authentication(
781749
assert response.json() == {"message": "Authentication successful"}
782750

783751

784-
# Scope-based authorization tests
785-
@patch("httpx.AsyncClient.post", new=mock_post_success_with_scope)
786-
async def test_scope_authorization_success(middleware_with_mocks, valid_api_key):
787-
"""Test that user with required scope can access protected endpoint"""
788-
middleware, mock_app = middleware_with_mocks
789-
mock_receive = AsyncMock()
790-
mock_send = AsyncMock()
791-
792-
scope = {
793-
"type": "http",
794-
"path": "/test/scoped",
795-
"method": "POST",
796-
"headers": [(b"authorization", f"Bearer {valid_api_key}".encode())],
797-
}
798-
799-
await middleware(scope, mock_receive, mock_send)
800-
801-
# Should call the downstream app (no 403 error sent)
802-
mock_app.assert_called_once_with(scope, mock_receive, mock_send)
803-
mock_send.assert_not_called()
804-
805-
806-
@patch("httpx.AsyncClient.post", new=mock_post_success_no_scope)
807-
async def test_scope_authorization_denied(middleware_with_mocks, valid_api_key):
808-
"""Test that user without required scope gets 403 access denied"""
809-
middleware, mock_app = middleware_with_mocks
810-
mock_receive = AsyncMock()
811-
mock_send = AsyncMock()
812-
813-
scope = {
814-
"type": "http",
815-
"path": "/test/scoped",
816-
"method": "POST",
817-
"headers": [(b"authorization", f"Bearer {valid_api_key}".encode())],
818-
}
819-
820-
await middleware(scope, mock_receive, mock_send)
821-
822-
# Should send 403 error, not call downstream app
823-
mock_app.assert_not_called()
824-
assert mock_send.call_count == 2 # start + body
825-
826-
# Check the response
827-
start_call = mock_send.call_args_list[0][0][0]
828-
assert start_call["status"] == 403
829-
830-
body_call = mock_send.call_args_list[1][0][0]
831-
body_text = body_call["body"].decode()
832-
assert "Access denied" in body_text
833-
assert "test.read" in body_text
834-
835-
836-
@patch("httpx.AsyncClient.post", new=mock_post_success_no_scope)
837-
async def test_public_endpoint_no_scope_required(middleware_with_mocks, valid_api_key):
838-
"""Test that public endpoints work without specific scopes"""
839-
middleware, mock_app = middleware_with_mocks
840-
mock_receive = AsyncMock()
841-
mock_send = AsyncMock()
842-
843-
scope = {
844-
"type": "http",
845-
"path": "/test/public",
846-
"method": "GET",
847-
"headers": [(b"authorization", f"Bearer {valid_api_key}".encode())],
848-
}
849-
850-
await middleware(scope, mock_receive, mock_send)
851-
852-
# Should call the downstream app (no error)
853-
mock_app.assert_called_once_with(scope, mock_receive, mock_send)
854-
mock_send.assert_not_called()
855-
856-
857-
async def test_scope_authorization_no_auth_disabled(middleware_with_mocks):
858-
"""Test that when auth is disabled (no user), scope checks are bypassed"""
859-
middleware, mock_app = middleware_with_mocks
860-
mock_receive = AsyncMock()
861-
mock_send = AsyncMock()
862-
863-
scope = {
864-
"type": "http",
865-
"path": "/test/scoped",
866-
"method": "POST",
867-
"headers": [], # No authorization header
868-
}
869-
870-
await middleware(scope, mock_receive, mock_send)
871-
872-
# Should send 401 auth error, not call downstream app
873-
mock_app.assert_not_called()
874-
assert mock_send.call_count == 2 # start + body
875-
876-
# Check the response
877-
start_call = mock_send.call_args_list[0][0][0]
878-
assert start_call["status"] == 401
879-
880-
body_call = mock_send.call_args_list[1][0][0]
881-
body_text = body_call["body"].decode()
882-
assert "Authentication required" in body_text
883-
884-
885-
def test_has_required_scope_function():
886-
"""Test the _has_required_scope function directly"""
887-
# Test user with required scope
888-
user_with_scope = User(principal="test-user", attributes={"scopes": ["test.read", "other.scope"]})
889-
assert _has_required_scope("test.read", user_with_scope)
890-
891-
# Test user without required scope
892-
user_without_scope = User(principal="test-user", attributes={"scopes": ["other.scope"]})
893-
assert not _has_required_scope("test.read", user_without_scope)
894-
895-
# Test user with no scopes attribute
896-
user_no_scopes = User(principal="test-user", attributes={})
897-
assert not _has_required_scope("test.read", user_no_scopes)
898-
899-
# Test no user (auth disabled)
900-
assert _has_required_scope("test.read", None)
901-
902-
903752
@pytest.fixture
904753
def mock_kubernetes_api_server():
905754
return "https://api.cluster.example.com:6443"

0 commit comments

Comments
 (0)