@@ -80,18 +80,100 @@ async def test_static_files_always_pass(self):
8080 call_next .assert_awaited_once_with (request )
8181
8282 @pytest .mark .asyncio
83- async def test_api_routes_skip_sso (self ):
84- """API routes should skip SSO and set user to None."""
83+ async def test_api_routes_skip_sso_by_default (self ):
84+ """API routes without @requires_sso should skip SSO and set user to None."""
8585 middleware = self ._make_middleware ()
8686 request = self ._make_request ("/api/v1/projects" )
8787 expected_response = Response (content = "ok" )
8888 call_next = AsyncMock (return_value = expected_response )
8989
90- result = await middleware .dispatch (request , call_next )
90+ with patch .object (middleware , "_route_requires_sso" , return_value = False ):
91+ result = await middleware .dispatch (request , call_next )
9192
9293 assert result is expected_response
9394 assert request .state .user is None
9495
96+ @pytest .mark .asyncio
97+ async def test_api_route_requires_sso_rejects_unauthenticated (self ):
98+ """API route marked @requires_sso must return 401 when there is no session user."""
99+ middleware = self ._make_middleware ()
100+ request = self ._make_request ("/api/metrics-explorer/metrics/minio" , session = {})
101+ call_next = AsyncMock ()
102+
103+ with patch .object (middleware , "_route_requires_sso" , return_value = True ):
104+ result = await middleware .dispatch (request , call_next )
105+
106+ assert result .status_code == 401
107+ call_next .assert_not_awaited ()
108+
109+ @pytest .mark .asyncio
110+ async def test_api_route_requires_sso_allows_authenticated (self ):
111+ """API route marked @requires_sso should pass through for an allowlisted session user."""
112+ middleware = self ._make_middleware ()
113+ user = {"email" : "test@example.com" }
114+ request = self ._make_request ("/api/metrics-explorer/metrics/minio" , session = {"user" : user })
115+ expected_response = Response (content = "ok" )
116+ call_next = AsyncMock (return_value = expected_response )
117+
118+ mock_user_service = MagicMock ()
119+ mock_user_service .is_email_allowed .return_value = True
120+
121+ with (
122+ patch .object (middleware , "_route_requires_sso" , return_value = True ),
123+ patch ("opi.middleware.authorization.get_user_service" , return_value = mock_user_service ),
124+ ):
125+ result = await middleware .dispatch (request , call_next )
126+
127+ assert result is expected_response
128+ assert request .state .user == user
129+
130+ @pytest .mark .asyncio
131+ async def test_api_route_requires_sso_blocks_non_allowlisted (self ):
132+ """API route marked @requires_sso must return 403 for a non-allowlisted user."""
133+ middleware = self ._make_middleware ()
134+ user = {"email" : "blocked@example.com" }
135+ request = self ._make_request ("/api/metrics-explorer/metrics/minio" , session = {"user" : user })
136+ call_next = AsyncMock ()
137+
138+ mock_user_service = MagicMock ()
139+ mock_user_service .is_email_allowed .return_value = False
140+
141+ with (
142+ patch .object (middleware , "_route_requires_sso" , return_value = True ),
143+ patch ("opi.middleware.authorization.get_user_service" , return_value = mock_user_service ),
144+ ):
145+ result = await middleware .dispatch (request , call_next )
146+
147+ assert result .status_code == 403
148+ call_next .assert_not_awaited ()
149+
150+ @pytest .mark .asyncio
151+ async def test_metrics_scrape_endpoint_skips_auth (self ):
152+ """The exact /metrics Prometheus scrape endpoint must remain public."""
153+ middleware = self ._make_middleware ()
154+ request = self ._make_request ("/metrics" )
155+ expected_response = Response (content = "ok" )
156+ call_next = AsyncMock (return_value = expected_response )
157+
158+ result = await middleware .dispatch (request , call_next )
159+
160+ assert result is expected_response
161+ call_next .assert_awaited_once_with (request )
162+
163+ @pytest .mark .asyncio
164+ async def test_metrics_explorer_is_not_skipped_by_metrics_prefix (self ):
165+ """Regression: /metrics-explorer must NOT be treated as public by the /metrics rule."""
166+ middleware = self ._make_middleware ()
167+ request = self ._make_request ("/metrics-explorer" , session = {})
168+ call_next = AsyncMock ()
169+
170+ with patch .object (middleware , "_route_requires_sso" , return_value = True ):
171+ result = await middleware .dispatch (request , call_next )
172+
173+ assert result .status_code == 302
174+ assert result .headers .get ("location" ) == "/auth/login"
175+ call_next .assert_not_awaited ()
176+
95177 @pytest .mark .asyncio
96178 async def test_unauthenticated_user_redirected (self ):
97179 """Should redirect to /auth/login when SSO required and no user."""
0 commit comments