@@ -43,6 +43,8 @@ class MockSettings:
4343 proxy_user_header = "X-Authenticated-User"
4444 require_token_expiration = False
4545 docs_allow_basic_auth = False
46+ require_user_in_db = True
47+ platform_admin_email = "admin@example.com"
4648
4749 return MockSettings ()
4850
@@ -112,10 +114,31 @@ async def test_proxy_auth_with_header(self, mock_settings, mock_request):
112114 mock_settings .trust_proxy_auth = True
113115 mock_settings .trust_proxy_auth_dangerously = True
114116 mock_request .headers = {"X-Authenticated-User" : "proxy-user" }
117+ mock_request .state = Mock ()
118+
119+ # Mock database user lookup
120+ mock_user = Mock ()
121+ mock_user .is_admin = False
122+ mock_user .email = "proxy-user"
115123
116124 with patch .object (vc , "settings" , mock_settings ):
117- result = await vc .require_auth (mock_request , None , None )
118- assert result == {"sub" : "proxy-user" , "source" : "proxy" , "token" : None }
125+ with patch ("mcpgateway.db.get_db" ) as mock_get_db :
126+ with patch ("mcpgateway.services.email_auth_service.EmailAuthService" ) as mock_auth_service :
127+ with patch ("mcpgateway.auth._resolve_teams_from_db" , new_callable = AsyncMock ) as mock_resolve_teams :
128+ # Setup mocks
129+ mock_db = Mock ()
130+ mock_get_db .return_value = iter ([mock_db ])
131+ mock_auth_service .return_value .get_user_by_email = AsyncMock (return_value = mock_user )
132+ mock_resolve_teams .return_value = ["team1" ]
133+
134+ result = await vc .require_auth (mock_request , None , None )
135+
136+ assert result ["sub" ] == "proxy-user"
137+ assert result ["source" ] == "proxy"
138+ assert result ["token" ] is None
139+ assert result ["is_admin" ] is False
140+ assert result ["teams" ] == ["team1" ]
141+ assert result ["email" ] == "proxy-user"
119142
120143 @pytest .mark .asyncio
121144 async def test_proxy_auth_without_header_raises_when_auth_required (self , mock_settings , mock_request ):
@@ -153,10 +176,31 @@ async def test_custom_proxy_header(self, mock_settings, mock_request):
153176 mock_settings .trust_proxy_auth_dangerously = True
154177 mock_settings .proxy_user_header = "X-Remote-User"
155178 mock_request .headers = {"X-Remote-User" : "custom-user" }
179+ mock_request .state = Mock ()
180+
181+ # Mock database user lookup
182+ mock_user = Mock ()
183+ mock_user .is_admin = False
184+ mock_user .email = "custom-user"
156185
157186 with patch .object (vc , "settings" , mock_settings ):
158- result = await vc .require_auth (mock_request , None , None )
159- assert result == {"sub" : "custom-user" , "source" : "proxy" , "token" : None }
187+ with patch ("mcpgateway.db.get_db" ) as mock_get_db :
188+ with patch ("mcpgateway.services.email_auth_service.EmailAuthService" ) as mock_auth_service :
189+ with patch ("mcpgateway.auth._resolve_teams_from_db" , new_callable = AsyncMock ) as mock_resolve_teams :
190+ # Setup mocks
191+ mock_db = Mock ()
192+ mock_get_db .return_value = iter ([mock_db ])
193+ mock_auth_service .return_value .get_user_by_email = AsyncMock (return_value = mock_user )
194+ mock_resolve_teams .return_value = ["team1" ]
195+
196+ result = await vc .require_auth (mock_request , None , None )
197+
198+ assert result ["sub" ] == "custom-user"
199+ assert result ["source" ] == "proxy"
200+ assert result ["token" ] is None
201+ assert result ["is_admin" ] is False
202+ assert result ["teams" ] == ["team1" ]
203+ assert result ["email" ] == "custom-user"
160204
161205 @pytest .mark .asyncio
162206 async def test_jwt_auth_with_proxy_enabled (self , mock_settings , mock_request ):
@@ -192,15 +236,90 @@ async def test_mixed_auth_scenario(self, mock_settings, mock_request):
192236 mock_settings .trust_proxy_auth = True
193237 mock_settings .trust_proxy_auth_dangerously = True
194238 mock_request .headers = {"X-Authenticated-User" : "proxy-user" }
239+ mock_request .state = Mock ()
195240
196241 # Create a valid JWT token
197242 token = jwt .encode ({"sub" : "jwt-user" }, mock_settings .jwt_secret_key , algorithm = "HS256" )
198243 creds = HTTPAuthorizationCredentials (scheme = "Bearer" , credentials = token )
199244
245+ # Mock database user lookup
246+ mock_user = Mock ()
247+ mock_user .is_admin = False
248+ mock_user .email = "proxy-user"
249+
250+ with patch .object (vc , "settings" , mock_settings ):
251+ with patch ("mcpgateway.db.get_db" ) as mock_get_db :
252+ with patch ("mcpgateway.services.email_auth_service.EmailAuthService" ) as mock_auth_service :
253+ with patch ("mcpgateway.auth._resolve_teams_from_db" , new_callable = AsyncMock ) as mock_resolve_teams :
254+ # Setup mocks
255+ mock_db = Mock ()
256+ mock_get_db .return_value = iter ([mock_db ])
257+ mock_auth_service .return_value .get_user_by_email = AsyncMock (return_value = mock_user )
258+ mock_resolve_teams .return_value = ["team1" ]
259+
260+ # When MCP client auth is disabled, proxy takes precedence
261+ result = await vc .require_auth (mock_request , creds , None )
262+
263+ assert result ["sub" ] == "proxy-user"
264+ assert result ["source" ] == "proxy"
265+ assert result ["token" ] is None
266+ assert result ["is_admin" ] is False
267+ assert result ["teams" ] == ["team1" ]
268+ assert result ["email" ] == "proxy-user"
269+
270+ @pytest .mark .asyncio
271+ async def test_proxy_auth_platform_admin_bootstrap (self , mock_settings , mock_request ):
272+ """Test proxy authentication with platform admin bootstrap when user not in DB."""
273+ mock_settings .mcp_client_auth_enabled = False
274+ mock_settings .trust_proxy_auth = True
275+ mock_settings .trust_proxy_auth_dangerously = True
276+ mock_settings .require_user_in_db = False
277+ mock_settings .platform_admin_email = "admin@example.com"
278+ mock_request .headers = {"X-Authenticated-User" : "admin@example.com" }
279+ mock_request .state = Mock ()
280+
281+ with patch .object (vc , "settings" , mock_settings ):
282+ with patch ("mcpgateway.db.get_db" ) as mock_get_db :
283+ with patch ("mcpgateway.services.email_auth_service.EmailAuthService" ) as mock_auth_service :
284+ # Setup mocks - user NOT found in DB
285+ mock_db = Mock ()
286+ mock_get_db .return_value = iter ([mock_db ])
287+ mock_auth_service .return_value .get_user_by_email = AsyncMock (return_value = None )
288+
289+ result = await vc .require_auth (mock_request , None , None )
290+
291+ # Should bootstrap platform admin
292+ assert result ["sub" ] == "admin@example.com"
293+ assert result ["source" ] == "proxy"
294+ assert result ["token" ] is None
295+ assert result ["is_admin" ] is True
296+ assert result ["teams" ] is None # Admin bypass
297+ assert result ["email" ] == "admin@example.com"
298+
299+ @pytest .mark .asyncio
300+ async def test_proxy_auth_user_not_found_raises_401 (self , mock_settings , mock_request ):
301+ """Test proxy authentication raises 401 when user not in DB and not platform admin."""
302+ mock_settings .mcp_client_auth_enabled = False
303+ mock_settings .trust_proxy_auth = True
304+ mock_settings .trust_proxy_auth_dangerously = True
305+ mock_settings .require_user_in_db = True
306+ mock_settings .platform_admin_email = "admin@example.com"
307+ mock_request .headers = {"X-Authenticated-User" : "unknown-user@example.com" }
308+ mock_request .state = Mock ()
309+
200310 with patch .object (vc , "settings" , mock_settings ):
201- # When MCP client auth is disabled, proxy takes precedence
202- result = await vc .require_auth (mock_request , creds , None )
203- assert result == {"sub" : "proxy-user" , "source" : "proxy" , "token" : None }
311+ with patch ("mcpgateway.db.get_db" ) as mock_get_db :
312+ with patch ("mcpgateway.services.email_auth_service.EmailAuthService" ) as mock_auth_service :
313+ # Setup mocks - user NOT found in DB
314+ mock_db = Mock ()
315+ mock_get_db .return_value = iter ([mock_db ])
316+ mock_auth_service .return_value .get_user_by_email = AsyncMock (return_value = None )
317+
318+ with pytest .raises (HTTPException ) as exc_info :
319+ await vc .require_auth (mock_request , None , None )
320+
321+ assert exc_info .value .status_code == 401
322+ assert "User not found in database" in exc_info .value .detail
204323
205324
206325class TestRBACProxyAuthentication :
0 commit comments