New token arch

This commit is contained in:
Urtzi Alfaro
2026-01-10 21:45:37 +01:00
parent cc53037552
commit bf1db7cb9e
26 changed files with 1751 additions and 107 deletions

View File

@@ -406,3 +406,73 @@ def service_only_access(func: Callable) -> Callable:
return await func(*args, **kwargs)
return wrapper
def require_verified_subscription_tier(
allowed_tiers: List[str],
verify_in_database: bool = False
):
"""
Subscription tier enforcement with optional database verification.
Args:
allowed_tiers: List of allowed subscription tiers
verify_in_database: If True, verify against database (for critical operations)
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
request = kwargs.get('request') or args[0]
# Get tier from gateway-injected header (from verified JWT)
header_tier = request.headers.get("x-subscription-tier", "starter").lower()
if header_tier not in [t.lower() for t in allowed_tiers]:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "subscription_required",
"message": f"This feature requires {', '.join(allowed_tiers)} subscription",
"current_tier": header_tier,
"required_tiers": allowed_tiers
}
)
# For critical operations, verify against database
if verify_in_database:
tenant_id = request.headers.get("x-tenant-id")
if tenant_id:
db_tier = await _verify_subscription_in_database(tenant_id)
if db_tier.lower() != header_tier:
logger.error(
"Subscription tier mismatch detected!",
header_tier=header_tier,
db_tier=db_tier,
tenant_id=tenant_id,
user_id=request.headers.get("x-user-id")
)
# Use database tier (authoritative)
if db_tier.lower() not in [t.lower() for t in allowed_tiers]:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "subscription_verification_failed",
"message": "Subscription tier verification failed"
}
)
return await func(*args, **kwargs)
return wrapper
return decorator
async def _verify_subscription_in_database(tenant_id: str) -> str:
"""
Direct database verification of subscription tier.
Used for critical operations as defense-in-depth.
"""
from shared.clients.subscription_client import SubscriptionClient
client = SubscriptionClient()
subscription = await client.get_subscription(tenant_id)
return subscription.get("plan", "starter")