|
21 | 21 | OAuth2JWKSConfig, |
22 | 22 | OAuth2TokenAuthConfig, |
23 | 23 | ) |
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 |
26 | 25 | from llama_stack.core.server.auth_providers import ( |
27 | 26 | get_attributes_from_claims, |
28 | 27 | ) |
@@ -143,11 +142,10 @@ def middleware_with_mocks(mock_auth_endpoint): |
143 | 142 | ) |
144 | 143 | middleware = AuthenticationMiddleware(mock_app, auth_config, {}) |
145 | 144 |
|
146 | | - # Mock the route_impls to simulate finding routes with required scopes |
147 | 145 | from llama_stack_api import WebMethod |
148 | 146 |
|
149 | 147 | 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"), |
151 | 149 | ("GET", "/test/public"): WebMethod(route="/test/public", method="GET"), |
152 | 150 | ("GET", "/health"): WebMethod(route="/health", method="GET", require_authentication=False), |
153 | 151 | ("GET", "/version"): WebMethod(route="/version", method="GET", require_authentication=False), |
@@ -193,36 +191,6 @@ async def mock_post_exception(*args, **kwargs): |
193 | 191 | raise Exception("Connection error") |
194 | 192 |
|
195 | 193 |
|
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 | | - |
226 | 194 | # HTTP Endpoint Tests |
227 | 195 | def test_missing_auth_header(http_client): |
228 | 196 | response = http_client.get("/test") |
@@ -781,125 +749,6 @@ def test_valid_introspection_with_custom_mapping_authentication( |
781 | 749 | assert response.json() == {"message": "Authentication successful"} |
782 | 750 |
|
783 | 751 |
|
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 | | - |
903 | 752 | @pytest.fixture |
904 | 753 | def mock_kubernetes_api_server(): |
905 | 754 | return "https://api.cluster.example.com:6443" |
|
0 commit comments