604 lines
21 KiB
Python
604 lines
21 KiB
Python
# ================================================================
|
|
# shared/auth/decorators.py - ENHANCED WITH ADMIN ROLE DECORATOR
|
|
# ================================================================
|
|
"""
|
|
Enhanced authentication decorators for microservices including admin role validation.
|
|
Designed to work with gateway authentication middleware and provide centralized
|
|
role-based access control across all services.
|
|
"""
|
|
|
|
from functools import wraps
|
|
from fastapi import HTTPException, status, Request, Depends
|
|
from fastapi.security import HTTPBearer
|
|
from typing import Callable, Optional, Dict, Any, List
|
|
import structlog
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
# Bearer token scheme for services that need it
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
def require_authentication(func: Callable) -> Callable:
|
|
"""
|
|
Decorator to require authentication - trusts gateway validation
|
|
Services behind the gateway should use this decorator
|
|
"""
|
|
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
# Find request object in arguments
|
|
request = None
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
break
|
|
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
|
|
if not request:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Request object not found"
|
|
)
|
|
|
|
# Check if user context exists (set by gateway)
|
|
if not hasattr(request.state, 'user') or not request.state.user:
|
|
# Check headers as fallback (for direct service calls in dev)
|
|
user_info = extract_user_from_headers(request)
|
|
if not user_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required"
|
|
)
|
|
request.state.user = user_info
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
def require_tenant_access(func: Callable) -> Callable:
|
|
"""Decorator to require tenant access"""
|
|
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
request = None
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
break
|
|
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
|
|
if not request or not hasattr(request.state, 'tenant_id'):
|
|
# Try to extract from headers
|
|
tenant_id = extract_tenant_from_headers(request)
|
|
if not tenant_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Tenant access required"
|
|
)
|
|
request.state.tenant_id = tenant_id
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
def require_role(role: str):
|
|
"""Decorator to require specific role"""
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
request = None
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
break
|
|
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
|
|
user = get_current_user(request)
|
|
user_role = user.get('role', '').lower()
|
|
|
|
if user_role != role.lower() and user_role != 'admin':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"{role} role required"
|
|
)
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
def require_admin_role(func: Callable) -> Callable:
|
|
"""
|
|
Decorator to require admin role - simplified version for FastAPI dependencies
|
|
|
|
This decorator ensures only users with 'admin' role can access the endpoint.
|
|
Can be used as a FastAPI dependency or function decorator.
|
|
|
|
Usage as dependency:
|
|
@router.delete("/admin/users/{user_id}")
|
|
async def delete_user(
|
|
user_id: str,
|
|
current_user = Depends(get_current_user_dep),
|
|
_admin_check = Depends(require_admin_role),
|
|
):
|
|
# Admin-only logic here
|
|
|
|
Usage as decorator:
|
|
@require_admin_role
|
|
@router.delete("/admin/users/{user_id}")
|
|
async def delete_user(...):
|
|
# Admin-only logic here
|
|
"""
|
|
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
# Find request object in arguments
|
|
request = None
|
|
current_user = None
|
|
|
|
# Extract request and current_user from arguments
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
elif isinstance(arg, dict) and 'user_id' in arg:
|
|
current_user = arg
|
|
|
|
# Check kwargs for request and current_user
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
if not current_user:
|
|
current_user = kwargs.get('current_user')
|
|
|
|
# If we still don't have current_user, try to get it from request
|
|
if not current_user and request:
|
|
current_user = get_current_user(request)
|
|
|
|
if not current_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required"
|
|
)
|
|
|
|
# Check if user has admin role
|
|
user_role = current_user.get('role', '').lower()
|
|
|
|
if user_role != 'admin':
|
|
logger.warning("Non-admin user attempted admin operation",
|
|
user_id=current_user.get('user_id'),
|
|
role=user_role)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin role required"
|
|
)
|
|
|
|
logger.info("Admin operation authorized",
|
|
user_id=current_user.get('user_id'),
|
|
endpoint=func.__name__)
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
def require_roles(allowed_roles: List[str]):
|
|
"""
|
|
Decorator to require one of multiple roles
|
|
|
|
Args:
|
|
allowed_roles: List of roles that are allowed to access the endpoint
|
|
|
|
Usage:
|
|
@require_roles(['admin', 'manager'])
|
|
@router.post("/sensitive-operation")
|
|
async def sensitive_operation(...):
|
|
# Only admins and managers can access
|
|
"""
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
request = None
|
|
current_user = None
|
|
|
|
# Extract request and current_user from arguments
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
elif isinstance(arg, dict) and 'user_id' in arg:
|
|
current_user = arg
|
|
|
|
# Check kwargs
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
if not current_user:
|
|
current_user = kwargs.get('current_user')
|
|
|
|
# Get user from request if not provided
|
|
if not current_user and request:
|
|
current_user = get_current_user(request)
|
|
|
|
if not current_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required"
|
|
)
|
|
|
|
# Check if user has one of the allowed roles
|
|
user_role = current_user.get('role', '').lower()
|
|
allowed_roles_lower = [role.lower() for role in allowed_roles]
|
|
|
|
if user_role not in allowed_roles_lower:
|
|
logger.warning("Unauthorized role attempted restricted operation",
|
|
user_id=current_user.get('user_id'),
|
|
user_role=user_role,
|
|
allowed_roles=allowed_roles)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"One of these roles required: {', '.join(allowed_roles)}"
|
|
)
|
|
|
|
logger.info("Role-based operation authorized",
|
|
user_id=current_user.get('user_id'),
|
|
user_role=user_role,
|
|
endpoint=func.__name__)
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
def require_tenant_admin(func: Callable) -> Callable:
|
|
"""
|
|
Decorator to require admin role within a specific tenant context
|
|
|
|
This checks that the user is an admin AND has access to the tenant
|
|
being operated on. Useful for tenant-scoped admin operations.
|
|
"""
|
|
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
request = None
|
|
current_user = None
|
|
|
|
# Extract request and current_user from arguments
|
|
for arg in args:
|
|
if isinstance(arg, Request):
|
|
request = arg
|
|
elif isinstance(arg, dict) and 'user_id' in arg:
|
|
current_user = arg
|
|
|
|
if not request:
|
|
request = kwargs.get('request')
|
|
if not current_user:
|
|
current_user = kwargs.get('current_user')
|
|
|
|
if not current_user and request:
|
|
current_user = get_current_user(request)
|
|
|
|
if not current_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required"
|
|
)
|
|
|
|
# Check admin role first
|
|
user_role = current_user.get('role', '').lower()
|
|
if user_role != 'admin':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin role required"
|
|
)
|
|
|
|
# Check tenant access
|
|
tenant_id = get_current_tenant_id(request) if request else None
|
|
if not tenant_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Tenant context required"
|
|
)
|
|
|
|
# Additional tenant admin validation could go here
|
|
# For now, we trust that admin users have access to operate on any tenant
|
|
|
|
logger.info("Tenant admin operation authorized",
|
|
user_id=current_user.get('user_id'),
|
|
tenant_id=tenant_id,
|
|
endpoint=func.__name__)
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
def get_current_user(request: Request) -> Dict[str, Any]:
|
|
"""Get current user from request state or headers"""
|
|
if hasattr(request.state, 'user') and request.state.user:
|
|
return request.state.user
|
|
|
|
# Fallback to headers (for dev/testing)
|
|
user_info = extract_user_from_headers(request)
|
|
if not user_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not authenticated"
|
|
)
|
|
|
|
return user_info
|
|
|
|
def get_current_tenant_id(request: Request) -> Optional[str]:
|
|
"""Get current tenant ID from request state or headers"""
|
|
if hasattr(request.state, 'tenant_id'):
|
|
return request.state.tenant_id
|
|
|
|
# Fallback to headers
|
|
return extract_tenant_from_headers(request)
|
|
|
|
def extract_user_from_headers(request: Request) -> Optional[Dict[str, Any]]:
|
|
"""Extract user information from forwarded headers (gateway sets these)"""
|
|
user_id = request.headers.get("x-user-id")
|
|
if not user_id:
|
|
return None
|
|
|
|
user_context = {
|
|
"user_id": user_id,
|
|
"email": request.headers.get("x-user-email", ""),
|
|
"role": request.headers.get("x-user-role", "user"),
|
|
"tenant_id": request.headers.get("x-tenant-id"),
|
|
"permissions": request.headers.get("X-User-Permissions", "").split(",") if request.headers.get("X-User-Permissions") else [],
|
|
"full_name": request.headers.get("x-user-full-name", ""),
|
|
"subscription_tier": request.headers.get("x-subscription-tier", "")
|
|
}
|
|
|
|
# ✅ ADD THIS: Handle service tokens properly
|
|
user_type = request.headers.get("x-user-type", "")
|
|
service_name = request.headers.get("x-service-name", "")
|
|
|
|
if user_type == "service" or service_name:
|
|
user_context.update({
|
|
"type": "service",
|
|
"service": service_name,
|
|
"role": "admin", # Service tokens always have admin role
|
|
"is_service": True
|
|
})
|
|
|
|
return user_context
|
|
|
|
def extract_tenant_from_headers(request: Request) -> Optional[str]:
|
|
"""Extract tenant ID from headers"""
|
|
return request.headers.get("x-tenant-id")
|
|
|
|
# ================================================================
|
|
# FASTAPI DEPENDENCY FUNCTIONS
|
|
# ================================================================
|
|
|
|
async def get_current_user_dep(request: Request) -> Dict[str, Any]:
|
|
"""FastAPI dependency to get current user - ENHANCED with detailed logging"""
|
|
try:
|
|
# Log all incoming headers for debugging 401 issues
|
|
logger.debug(
|
|
"Authentication attempt",
|
|
path=request.url.path,
|
|
method=request.method,
|
|
has_auth_header=bool(request.headers.get("authorization")),
|
|
has_x_user_id=bool(request.headers.get("x-user-id")),
|
|
has_x_user_type=bool(request.headers.get("x-user-type")),
|
|
has_x_service_name=bool(request.headers.get("x-service-name")),
|
|
x_user_type=request.headers.get("x-user-type", ""),
|
|
x_service_name=request.headers.get("x-service-name", ""),
|
|
client_ip=request.client.host if request.client else "unknown"
|
|
)
|
|
|
|
user = get_current_user(request)
|
|
|
|
logger.info(
|
|
"User authenticated successfully",
|
|
user_id=user.get("user_id"),
|
|
user_type=user.get("type", "user"),
|
|
is_service=user.get("type") == "service",
|
|
role=user.get("role"),
|
|
path=request.url.path
|
|
)
|
|
|
|
return user
|
|
|
|
except HTTPException as e:
|
|
logger.warning(
|
|
"Authentication failed - 401",
|
|
path=request.url.path,
|
|
status_code=e.status_code,
|
|
detail=e.detail,
|
|
has_x_user_id=bool(request.headers.get("x-user-id")),
|
|
x_user_type=request.headers.get("x-user-type", "none"),
|
|
x_service_name=request.headers.get("x-service-name", "none"),
|
|
client_ip=request.client.host if request.client else "unknown"
|
|
)
|
|
raise
|
|
|
|
async def get_current_tenant_id_dep(request: Request) -> Optional[str]:
|
|
"""FastAPI dependency to get current tenant ID"""
|
|
return get_current_tenant_id(request)
|
|
|
|
async def require_admin_role_dep(
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
FastAPI dependency that requires admin role
|
|
|
|
Usage:
|
|
@router.delete("/admin/users/{user_id}")
|
|
async def delete_user(
|
|
user_id: str,
|
|
admin_user: Dict[str, Any] = Depends(require_admin_role_dep)
|
|
):
|
|
# admin_user is guaranteed to have admin role
|
|
"""
|
|
|
|
user_role = current_user.get('role', '').lower()
|
|
|
|
if user_role != 'admin':
|
|
logger.warning("Non-admin user attempted admin operation",
|
|
user_id=current_user.get('user_id'),
|
|
role=user_role)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin role required"
|
|
)
|
|
|
|
logger.info("Admin operation authorized via dependency",
|
|
user_id=current_user.get('user_id'))
|
|
|
|
return current_user
|
|
|
|
async def require_roles_dep(allowed_roles: List[str]):
|
|
"""
|
|
FastAPI dependency factory that requires one of multiple roles
|
|
|
|
Usage:
|
|
require_manager_or_admin = require_roles_dep(['admin', 'manager'])
|
|
|
|
@router.post("/sensitive-operation")
|
|
async def sensitive_operation(
|
|
user: Dict[str, Any] = Depends(require_manager_or_admin)
|
|
):
|
|
# Only admins and managers can access
|
|
"""
|
|
|
|
async def check_roles(
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep)
|
|
) -> Dict[str, Any]:
|
|
user_role = current_user.get('role', '').lower()
|
|
allowed_roles_lower = [role.lower() for role in allowed_roles]
|
|
|
|
if user_role not in allowed_roles_lower:
|
|
logger.warning("Unauthorized role attempted restricted operation",
|
|
user_id=current_user.get('user_id'),
|
|
user_role=user_role,
|
|
allowed_roles=allowed_roles)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"One of these roles required: {', '.join(allowed_roles)}"
|
|
)
|
|
|
|
logger.info("Role-based operation authorized via dependency",
|
|
user_id=current_user.get('user_id'),
|
|
user_role=user_role)
|
|
|
|
return current_user
|
|
|
|
return check_roles
|
|
|
|
# ================================================================
|
|
# UTILITY FUNCTIONS FOR ROLE CHECKING
|
|
# ================================================================
|
|
|
|
def is_admin_user(user: Dict[str, Any]) -> bool:
|
|
"""Check if user has admin role"""
|
|
return user.get('role', '').lower() == 'admin'
|
|
|
|
def is_user_in_roles(user: Dict[str, Any], allowed_roles: List[str]) -> bool:
|
|
"""Check if user has one of the allowed roles"""
|
|
user_role = user.get('role', '').lower()
|
|
allowed_roles_lower = [role.lower() for role in allowed_roles]
|
|
return user_role in allowed_roles_lower
|
|
|
|
def get_user_permissions(user: Dict[str, Any]) -> List[str]:
|
|
"""Get user permissions list"""
|
|
return user.get('permissions', [])
|
|
|
|
def has_permission(user: Dict[str, Any], permission: str) -> bool:
|
|
"""Check if user has specific permission"""
|
|
permissions = get_user_permissions(user)
|
|
return permission in permissions
|
|
|
|
# ================================================================
|
|
# ADVANCED ROLE DECORATORS
|
|
# ================================================================
|
|
|
|
def require_permission(permission: str):
|
|
"""
|
|
Decorator to require specific permission
|
|
|
|
Usage:
|
|
@require_permission('delete_users')
|
|
@router.delete("/users/{user_id}")
|
|
async def delete_user(...):
|
|
# Only users with 'delete_users' permission can access
|
|
"""
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
async def wrapper(*args, **kwargs):
|
|
current_user = None
|
|
|
|
# Extract current_user from arguments
|
|
for arg in args:
|
|
if isinstance(arg, dict) and 'user_id' in arg:
|
|
current_user = arg
|
|
break
|
|
|
|
if not current_user:
|
|
current_user = kwargs.get('current_user')
|
|
|
|
if not current_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication required"
|
|
)
|
|
|
|
# Check permission
|
|
if not has_permission(current_user, permission):
|
|
# Admins bypass permission checks
|
|
if not is_admin_user(current_user):
|
|
logger.warning("User lacks required permission",
|
|
user_id=current_user.get('user_id'),
|
|
required_permission=permission,
|
|
user_permissions=get_user_permissions(current_user))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Permission '{permission}' required"
|
|
)
|
|
|
|
logger.info("Permission-based operation authorized",
|
|
user_id=current_user.get('user_id'),
|
|
permission=permission)
|
|
|
|
return await func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
# Export all decorators and functions
|
|
__all__ = [
|
|
# Main decorators
|
|
'require_authentication',
|
|
'require_tenant_access',
|
|
'require_role',
|
|
'require_admin_role',
|
|
'require_roles',
|
|
'require_tenant_admin',
|
|
'require_permission',
|
|
|
|
# FastAPI dependencies
|
|
'get_current_user_dep',
|
|
'get_current_tenant_id_dep',
|
|
'require_admin_role_dep',
|
|
'require_roles_dep',
|
|
|
|
# Utility functions
|
|
'get_current_user',
|
|
'get_current_tenant_id',
|
|
'extract_user_from_headers',
|
|
'extract_tenant_from_headers',
|
|
'is_admin_user',
|
|
'is_user_in_roles',
|
|
'get_user_permissions',
|
|
'has_permission'
|
|
] |