83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
"""Human requests use user identities; tokens are reserved for scoped machines."""
|
|
import hmac
|
|
import os
|
|
from ipaddress import ip_address, ip_network
|
|
|
|
from fastapi import Header, HTTPException, Request
|
|
|
|
from .config import settings
|
|
|
|
|
|
def token_is_strong(value: str | None, *, minimum_length: int = 24) -> bool:
|
|
return bool(value and len(value) >= minimum_length and value.strip().lower() not in {"change-me", "changeme", "dev", "test", "dev-voice-token"})
|
|
|
|
|
|
def _extract_bearer_token(authorization: str | None) -> str:
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
raise HTTPException(401, "Authorization header missing")
|
|
return authorization[len("Bearer "):]
|
|
|
|
|
|
def require_auth_token(provided_token: str | None, expected_token: str | None, scope: str):
|
|
if not expected_token:
|
|
raise HTTPException(503, f"{scope} authentication is not configured")
|
|
if not hmac.compare_digest(_extract_bearer_token(provided_token), expected_token):
|
|
raise HTTPException(403, f"Invalid {scope} token")
|
|
|
|
|
|
def _request_from_trusted_operator_proxy(request: Request) -> bool:
|
|
if not settings.operator_proxy_auth_enabled or not settings.operator_proxy_trusted_networks:
|
|
return False
|
|
try:
|
|
client_ip = ip_address(request.client.host if request.client else "")
|
|
except ValueError:
|
|
return False
|
|
for configured_network in settings.operator_proxy_trusted_networks:
|
|
try:
|
|
if client_ip in ip_network(configured_network, strict=False):
|
|
return True
|
|
except ValueError:
|
|
continue
|
|
return False
|
|
|
|
|
|
def require_operator_request(request: Request) -> str:
|
|
# Compatibility for existing governed routes; no shared token or unlock.
|
|
from ..services.user_access import authorize_request
|
|
user = authorize_request(request)
|
|
return f"{user.provider}:{user.subject}"
|
|
|
|
|
|
def require_service_request(request: Request, service: str) -> str:
|
|
if service == "mcp":
|
|
enabled, expected = settings.mcp_enabled, settings.mcp_token
|
|
elif service == "n8n":
|
|
enabled, expected = settings.n8n_enabled, settings.n8n_token
|
|
elif service == "voice":
|
|
enabled, expected = True, settings.voice_token or os.getenv("APP_OPS_VOICE_TOKEN")
|
|
else:
|
|
raise HTTPException(403, "Unknown service identity")
|
|
if not enabled:
|
|
raise HTTPException(404, f"{service} integration is disabled")
|
|
if not expected or ((service != 'voice' or settings.environment == 'production') and not token_is_strong(expected)):
|
|
raise HTTPException(503, f"{service} authentication is not safely configured")
|
|
require_auth_token(request.headers.get("Authorization"), expected, service)
|
|
return service
|
|
|
|
|
|
def n8n_auth(request: Request) -> str:
|
|
return require_service_request(request, "n8n")
|
|
|
|
|
|
def operator_auth(request: Request) -> str:
|
|
return require_operator_request(request)
|
|
|
|
|
|
def voice_auth(request: Request, authorization: str | None = Header(default=None, alias="Authorization")):
|
|
if authorization:
|
|
expected = settings.voice_token or os.getenv("APP_OPS_VOICE_TOKEN")
|
|
require_auth_token(authorization, expected, "voice")
|
|
else:
|
|
require_operator_request(request)
|
|
return True
|