Files
bakery-ia/services/tenant/app/api/tenants.py

275 lines
9.9 KiB
Python
Raw Normal View History

2025-07-19 17:49:03 +02:00
"""
2025-10-06 15:27:01 +02:00
Tenant API - ATOMIC operations
Handles basic CRUD operations for tenants
2025-07-19 17:49:03 +02:00
"""
import structlog
2025-11-05 13:34:56 +01:00
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
from typing import Dict, Any, List
2025-08-08 09:08:41 +02:00
from uuid import UUID
2025-07-19 17:49:03 +02:00
2025-10-06 15:27:01 +02:00
from app.schemas.tenants import TenantResponse, TenantUpdate
2025-08-08 09:08:41 +02:00
from app.services.tenant_service import EnhancedTenantService
2025-10-06 15:27:01 +02:00
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import admin_role_required
2025-10-06 15:27:01 +02:00
from shared.routing.route_builder import RouteBuilder
2025-08-08 09:08:41 +02:00
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
2025-07-19 17:49:03 +02:00
logger = structlog.get_logger()
router = APIRouter()
2025-10-06 15:27:01 +02:00
route_builder = RouteBuilder("tenants")
2025-07-19 17:49:03 +02:00
2025-08-08 09:08:41 +02:00
# Dependency injection for enhanced tenant service
def get_enhanced_tenant_service():
2025-08-15 22:40:19 +02:00
try:
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
return EnhancedTenantService(database_manager)
except Exception as e:
logger.error("Failed to create enhanced tenant service", error=str(e))
raise HTTPException(status_code=500, detail="Service initialization failed")
2025-08-08 09:08:41 +02:00
2025-11-05 13:34:56 +01:00
@router.get(route_builder.build_base_route("", include_tenant_prefix=False), response_model=List[TenantResponse])
@track_endpoint_metrics("tenants_list")
async def get_active_tenants(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get all active tenants - Available to service accounts and admins"""
logger.info(
"Get active tenants request received",
skip=skip,
limit=limit,
user_id=current_user.get("user_id"),
user_type=current_user.get("type", "user"),
is_service=current_user.get("type") == "service",
role=current_user.get("role"),
service_name=current_user.get("service", "none")
)
# Allow service accounts to call this endpoint
if current_user.get("type") != "service":
# For non-service users, could add additional role checks here if needed
logger.debug(
"Non-service user requesting active tenants",
user_id=current_user.get("user_id"),
role=current_user.get("role")
)
tenants = await tenant_service.get_active_tenants(skip=skip, limit=limit)
logger.debug(
"Get active tenants successful",
count=len(tenants),
skip=skip,
limit=limit
)
return tenants
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False), response_model=TenantResponse)
2025-08-08 09:08:41 +02:00
@track_endpoint_metrics("tenant_get")
2025-10-06 15:27:01 +02:00
async def get_tenant(
2025-07-26 18:46:52 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
2025-07-21 14:41:33 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-08-08 09:08:41 +02:00
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
2025-07-19 17:49:03 +02:00
):
"""Get tenant by ID - ATOMIC operation - ENHANCED with logging"""
logger.info(
"Tenant GET request received",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id"),
user_type=current_user.get("type", "user"),
is_service=current_user.get("type") == "service",
role=current_user.get("role"),
service_name=current_user.get("service", "none")
)
2025-10-06 15:27:01 +02:00
2025-08-08 09:08:41 +02:00
tenant = await tenant_service.get_tenant_by_id(str(tenant_id))
2025-07-19 17:49:03 +02:00
if not tenant:
logger.warning(
"Tenant not found",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id")
)
2025-07-19 17:49:03 +02:00
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
logger.debug(
"Tenant GET request successful",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id")
)
2025-08-08 09:08:41 +02:00
return tenant
2025-10-06 15:27:01 +02:00
@router.put(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False), response_model=TenantResponse)
@admin_role_required
2025-10-06 15:27:01 +02:00
async def update_tenant(
2025-07-19 17:49:03 +02:00
update_data: TenantUpdate,
2025-07-26 18:46:52 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
2025-07-21 14:41:33 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-08-08 09:08:41 +02:00
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
2025-07-19 17:49:03 +02:00
):
"""Update tenant information - ATOMIC operation (Admin+ only)"""
2025-07-21 14:41:33 +02:00
2025-07-19 17:49:03 +02:00
try:
2025-08-08 09:08:41 +02:00
result = await tenant_service.update_tenant(
2025-10-06 15:27:01 +02:00
str(tenant_id),
update_data,
2025-08-08 09:08:41 +02:00
current_user["user_id"]
)
2025-07-19 17:49:03 +02:00
return result
2025-10-06 15:27:01 +02:00
2025-07-19 17:49:03 +02:00
except HTTPException:
raise
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Tenant update failed",
tenant_id=str(tenant_id),
user_id=current_user["user_id"],
error=str(e))
2025-07-19 17:49:03 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Tenant update failed"
)
2025-10-31 11:54:19 +01:00
2025-12-05 20:07:01 +01:00
@router.get(route_builder.build_base_route("user/{user_id}/tenants", include_tenant_prefix=False), response_model=List[TenantResponse])
@track_endpoint_metrics("user_tenants_list")
async def get_user_tenants(
user_id: str = Path(..., description="User ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get all tenants accessible by a user"""
logger.info(
"Get user tenants request received",
user_id=user_id,
requesting_user=current_user.get("user_id"),
is_demo=current_user.get("is_demo", False)
2025-12-05 20:07:01 +01:00
)
# Allow demo users to access tenant information for demo-user
is_demo_user = current_user.get("is_demo", False)
is_service_account = current_user.get("type") == "service"
# For demo sessions, when frontend requests with "demo-user", use the actual demo owner ID
actual_user_id = user_id
if is_demo_user and user_id == "demo-user":
actual_user_id = current_user.get("user_id")
logger.info(
"Demo session: mapping demo-user to actual owner",
requested_user_id=user_id,
actual_user_id=actual_user_id
)
if current_user.get("user_id") != actual_user_id and not is_service_account and not (is_demo_user and user_id == "demo-user"):
2025-12-05 20:07:01 +01:00
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only access own tenants"
)
try:
CRITICAL: Add demo session isolation to prevent cross-session data leakage This commit fixes a critical security issue where multiple concurrent demo sessions would see each other's data due to sharing the same demo user IDs. ## The Problem: When two enterprise demo sessions run simultaneously: - Session A: user_id=Director, tenants=[parent_A, child_A1, child_A2] - Session B: user_id=Director, tenants=[parent_B, child_B1, child_B2] The endpoint /api/v1/tenants/user/{user_id}/tenants was querying by user_id only, so Session A would see BOTH its own tenants AND Session B's tenants! ## The Solution: Added demo_session_id filtering to get_user_tenants endpoint: - For demo sessions, use get_virtual_tenants_for_session(demo_session_id) - This filters tenants by the demo_session_id field (set during cloning) - Each session now sees ONLY its own virtual tenants ## Implementation: services/tenant/app/api/tenants.py (lines 180-194): - Check if user is_demo - Extract demo_session_id from current_user context (set by gateway) - Call get_virtual_tenants_for_session() instead of get_user_tenants() - This method filters by: demo_session_id + is_active + account_type ## Database Schema: The tenants table has a demo_session_id column (indexed) that links each virtual tenant to its specific demo session. This is set during tenant cloning in internal_demo.py. ## Impact: ✅ Complete isolation between concurrent demo sessions ✅ Users only see their own session's data ✅ No performance impact (demo_session_id is indexed) ✅ Backward compatible (non-demo users unchanged) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 16:03:23 +01:00
# For demo sessions, use session-specific filtering to prevent cross-session data leakage
if is_demo_user:
demo_session_id = current_user.get("demo_session_id")
demo_account_type = current_user.get("demo_account_type", "professional")
if demo_session_id:
# Get only tenants for this specific demo session
tenants = await tenant_service.get_virtual_tenants_for_session(demo_session_id, demo_account_type)
logger.debug(
"Get demo session tenants successful",
user_id=user_id,
demo_session_id=demo_session_id,
tenant_count=len(tenants)
)
return tenants
else:
logger.warning(
"Demo user without session ID - falling back to regular user tenants",
user_id=actual_user_id
)
# Regular users or demo fallback: get tenants by ownership and membership
tenants = await tenant_service.get_user_tenants(actual_user_id)
2025-12-05 20:07:01 +01:00
logger.debug(
"Get user tenants successful",
user_id=user_id,
tenant_count=len(tenants)
)
return tenants
except HTTPException:
raise
except Exception as e:
logger.error("Get user tenants failed",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user tenants"
)
2025-10-31 11:54:19 +01:00
@router.delete(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False))
@track_endpoint_metrics("tenant_delete")
async def delete_tenant(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Delete tenant and all associated data - ATOMIC operation (Owner/Admin or System only)"""
logger.info(
"Tenant DELETE request received",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id"),
user_type=current_user.get("type", "user"),
is_service=current_user.get("type") == "service",
role=current_user.get("role"),
service_name=current_user.get("service", "none")
)
try:
# Allow internal service calls to bypass admin check
skip_admin_check = current_user.get("type") == "service"
result = await tenant_service.delete_tenant(
str(tenant_id),
requesting_user_id=current_user.get("user_id"),
skip_admin_check=skip_admin_check
)
logger.info(
"Tenant DELETE request successful",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id"),
deleted_items=result.get("deleted_items")
)
return {
"message": "Tenant deleted successfully",
"summary": result
}
except HTTPException:
raise
except Exception as e:
logger.error("Tenant deletion failed",
tenant_id=str(tenant_id),
user_id=current_user.get("user_id"),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Tenant deletion failed"
)