Skip to content

Commit d144578

Browse files
Bogdan-Marius-Catanusjonpspri
authored andcommitted
fix: resolve proxy auth database lookup for team/admin context
- Add database lookup in proxy authentication to resolve user teams and admin status - Proxy auth now queries DB via EmailAuthService.get_user_by_email() - Resolves teams using _resolve_teams_from_db() (same pattern as session tokens) - Admin users get teams=None (admin bypass), regular users get team list - Platform admin bootstrap supported when REQUIRE_USER_IN_DB=false - Cache enriched payload in request.state._jwt_verified_payload for downstream use - Add comprehensive test coverage for proxy auth scenarios - Tests cover: normal users, admin users, platform admin bootstrap, user not found Fixes proxy authentication bug where users only had public-only access regardless of their actual database permissions. Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com>
1 parent d773bd8 commit d144578

2 files changed

Lines changed: 172 additions & 8 deletions

File tree

mcpgateway/utils/verify_credentials.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,52 @@ async def require_auth(request: Request, credentials: Optional[HTTPAuthorization
486486
# Extract user from proxy header
487487
proxy_user = request.headers.get(settings.proxy_user_header)
488488
if proxy_user:
489-
return {"sub": proxy_user, "source": "proxy", "token": None} # nosec B105 - None is not a password
489+
# First-Party
490+
from mcpgateway.auth import _resolve_teams_from_db
491+
from mcpgateway.db import get_db
492+
from mcpgateway.services.email_auth_service import EmailAuthService
493+
494+
# Query database for user info
495+
db = next(get_db())
496+
try:
497+
auth_service = EmailAuthService(db)
498+
user_info = await auth_service.get_user_by_email(proxy_user)
499+
500+
if user_info:
501+
# Resolve teams from DB (returns None for admin bypass, [] for no teams, or list of team IDs)
502+
token_teams = await _resolve_teams_from_db(proxy_user, user_info)
503+
is_admin = user_info.is_admin
504+
505+
# Build enriched payload similar to session tokens
506+
payload = {
507+
"sub": proxy_user,
508+
"source": "proxy",
509+
"token": None,
510+
"is_admin": is_admin,
511+
"teams": token_teams, # None for admin bypass, [] for public-only, or list of team IDs
512+
"email": proxy_user,
513+
}
514+
515+
# Cache in request state for downstream use (same pattern as JWT tokens)
516+
request.state._jwt_verified_payload = (None, payload)
517+
518+
return payload
519+
else:
520+
# User not in DB - handle based on REQUIRE_USER_IN_DB setting
521+
platform_admin_email = getattr(settings, "platform_admin_email", "admin@example.com")
522+
if not settings.require_user_in_db and proxy_user == platform_admin_email:
523+
# Platform admin bootstrap
524+
payload = {"sub": proxy_user, "source": "proxy", "token": None, "is_admin": True, "teams": None, "email": proxy_user} # Admin bypass
525+
request.state._jwt_verified_payload = (None, payload)
526+
return payload
527+
else:
528+
raise HTTPException(
529+
status_code=status.HTTP_401_UNAUTHORIZED,
530+
detail="User not found in database",
531+
headers={"WWW-Authenticate": "Bearer"},
532+
)
533+
finally:
534+
db.close()
490535
# No proxy header - check auth_required (matches RBAC/WebSocket behavior)
491536
if settings.auth_required:
492537
raise HTTPException(

tests/unit/mcpgateway/utils/test_proxy_auth.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

206325
class TestRBACProxyAuthentication:

0 commit comments

Comments
 (0)