-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathmiddleware_utils.py
More file actions
197 lines (170 loc) · 6.51 KB
/
Copy pathmiddleware_utils.py
File metadata and controls
197 lines (170 loc) · 6.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
from __future__ import annotations
from fastapi import Request
from fastapi.responses import JSONResponse
from sqlalchemy import select
from src.adapters.authentication.adapter_agentex_authn_proxy import (
AgentexAuthenticationProxy,
)
from src.adapters.orm import AgentAPIKeyORM, AgentORM
from src.config.dependencies import middleware_async_read_only_session_maker
from src.utils.logging import make_logger
logger = make_logger(__name__)
# Routes (and their prefixes) that bypass authentication
WHITELISTED_ROUTES: set[str] = {
"/agents/register",
"/agents/forward",
"/docs",
"/api",
"/openapi.json",
"/redoc",
"/favicon.ico",
"/health",
"/healthcheck",
"/healthz",
"/readyz",
"/ping",
"/echo",
# Channels (webhook, …) bypass agentex API-key auth and verify a per-route
# shared secret / HMAC inside the channel instead.
"/channels/webhook",
}
DROP_HEADERS: set[str] = {
"content-length",
"host",
"connection",
"transfer-encoding",
"expect",
}
def is_whitelisted_route(
path: str, whitelisted_routes: set[str] = WHITELISTED_ROUTES
) -> bool:
"""Check if a route path is whitelisted (bypasses authentication)."""
# Boundary-aware prefix match: a whitelisted route only matches the route
# itself or a sub-path under it (route + "/..."). Plain startswith() would
# let "/agents/register" whitelist "/agents/register-build" too, which must
# stay authenticated so it can carry the caller's principal (owner grant).
return path in whitelisted_routes or any(
path == route or path.startswith(route + "/") for route in whitelisted_routes
)
async def verify_agent_identity(
request: Request, agent_identity: str
) -> JSONResponse | None:
"""
Verify agent identity against the database.
Returns:
None if agent is valid (authentication should proceed)
JSONResponse with error if agent is invalid or verification fails
"""
try:
# Try to get the agent from the repository
# Using a separate sessionmaker and sqlalchemy pool so it never gets blocked by the application
AsyncReadOnlySessionMaker = middleware_async_read_only_session_maker()
async with AsyncReadOnlySessionMaker() as session:
agent = await session.scalar(select(AgentORM).filter_by(id=agent_identity))
if agent:
request.state.agent_identity = agent.id
logger.info(f"Authentication gateway verified agent ID {agent.id}")
return None # Agent is valid, continue processing
else:
return JSONResponse(
status_code=401,
content={"detail": "Agent Unauthorized"},
)
except Exception as e:
logger.error(f"Error verifying agent identity: {e}")
return JSONResponse(
status_code=500,
content={"detail": "Agent authorization failed"},
)
async def verify_agent_api_key(
request: Request, agent_api_key: str
) -> JSONResponse | None:
"""
Verify agent API key against the database.
Returns:
None if agent is valid (authentication should proceed)
JSONResponse with error if agent is invalid or verification fails
"""
try:
# Try to get the agent from the repository
# Using a separate sessionmaker and sqlalchemy pool so it never gets blocked by the application
AsyncReadOnlySessionMaker = middleware_async_read_only_session_maker()
async with AsyncReadOnlySessionMaker() as session:
resolved_api_key = await session.scalar(
select(AgentAPIKeyORM).filter_by(api_key=agent_api_key)
)
if resolved_api_key:
request.state.agent_identity = resolved_api_key.agent_id
logger.info(
f"Authentication gateway verified API key for agent ID {resolved_api_key.agent_id}"
)
return None # Agent API key is valid, continue processing
else:
logger.warning("Invalid agent API key provided")
return JSONResponse(
status_code=401,
content={"detail": "Agent Unauthorized"},
)
except Exception as e:
logger.error(f"Error verifying agent API key: {e}")
return JSONResponse(
status_code=500,
content={"detail": "Agent authorization failed"},
)
async def verify_auth_gateway(
request: Request, auth_gateway: AgentexAuthenticationProxy
) -> JSONResponse | None:
"""
Verify request through the authentication gateway.
Returns:
None if authentication successful (sets principal_context on request.state)
JSONResponse with error if authentication fails
"""
headers_dict = get_request_headers_to_forward(request)
try:
principal_context = await auth_gateway.verify_headers(headers_dict)
request.state.principal_context = principal_context
# Get route information
route_path = request.url.path
method = request.method
logger.info(
"[authentication_middleware] Request authenticated successfully for %s %s "
"(user_id=%s, account_id=%s)",
method,
route_path,
getattr(principal_context, "user_id", None),
getattr(principal_context, "account_id", None),
)
return None # Authentication successful
except Exception as exc:
logger.error(
"[authentication_middleware] Request for %s %s failed with %s",
request.method,
request.url.path,
str(exc),
)
return JSONResponse(
status_code=401,
content={"detail": "Unauthorized"},
)
def get_request_headers_to_forward(
request: Request,
exclude_headers: set[str] = DROP_HEADERS,
) -> dict[str, str]:
"""
Get headers to forward in a request, excluding specified headers.
Args:
request: The incoming request object.
exclude_headers: Set of header names to exclude from forwarding.
Returns:
Dictionary of headers to forward.
"""
return {
name.lower(): value
for name, value in request.headers.items()
if name.lower() not in exclude_headers
}
def resolve_authorization_enabled(env_value: str) -> bool:
"""Resolve whether authorization is enabled based on environment variable."""
logger.info(f"Authorization URL: {env_value}")
return bool(env_value)