New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -353,7 +353,8 @@ def extract_user_from_headers(request: Request) -> Optional[Dict[str, Any]]:
"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", "")
"subscription_tier": request.headers.get("x-subscription-tier", ""),
"is_demo": request.headers.get("x-is-demo", "").lower() == "true"
}
# ✅ ADD THIS: Handle service tokens properly

View File

@@ -73,21 +73,26 @@ class TenantAccessManager:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/access/{user_id}"
)
has_access = response.status_code == 200
# If direct access check fails, check hierarchical access
if not has_access:
hierarchical_access = await self._check_hierarchical_access(user_id, tenant_id)
has_access = hierarchical_access
# Cache result (5 minutes)
if self.redis_client:
try:
await self.redis_client.setex(cache_key, 300, "true" if has_access else "false")
except Exception as cache_error:
logger.warning(f"Cache set failed: {cache_error}")
logger.debug(f"Tenant access check",
user_id=user_id,
tenant_id=tenant_id,
logger.debug(f"Tenant access check",
user_id=user_id,
tenant_id=tenant_id,
has_access=has_access)
return has_access
except asyncio.TimeoutError:
@@ -102,17 +107,193 @@ class TenantAccessManager:
logger.error(f"Gateway tenant access verification failed: {e}")
# Fail open for availability (let service handle detailed check)
return True
async def _check_hierarchical_access(self, user_id: str, tenant_id: str) -> bool:
"""
Check if user has hierarchical access (parent tenant access to child)
Args:
user_id: User ID to verify
tenant_id: Target tenant ID to check access for
Returns:
bool: True if user has hierarchical access to the tenant
"""
try:
async with httpx.AsyncClient(timeout=3.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/hierarchy"
)
if response.status_code == 200:
hierarchy_data = response.json()
parent_tenant_id = hierarchy_data.get("parent_tenant_id")
# If this is a child tenant, check if user has access to parent
if parent_tenant_id:
# Check if user has access to parent tenant
parent_access = await self._check_parent_access(user_id, parent_tenant_id)
if parent_access:
# For aggregated data only, allow parent access to child
# Detailed child data requires direct access
user_role = await self.get_user_role_in_tenant(user_id, parent_tenant_id)
if user_role in ["owner", "admin", "network_admin"]:
return True
return False
except Exception as e:
logger.error(f"Failed to check hierarchical access: {e}")
return False
async def _check_parent_access(self, user_id: str, parent_tenant_id: str) -> bool:
"""
Check if user has access to parent tenant (owner, admin, or network_admin role)
Args:
user_id: User ID
parent_tenant_id: Parent tenant ID
Returns:
bool: True if user has access to parent tenant
"""
user_role = await self.get_user_role_in_tenant(user_id, parent_tenant_id)
return user_role in ["owner", "admin", "network_admin"]
async def verify_hierarchical_access(self, user_id: str, tenant_id: str) -> dict:
"""
Verify hierarchical access and return access type and permissions
Args:
user_id: User ID
tenant_id: Target tenant ID
Returns:
dict: Access information including access_type, can_view_children, etc.
"""
# First check direct access
direct_access = await self._check_direct_access(user_id, tenant_id)
if direct_access:
return {
"access_type": "direct",
"has_access": True,
"can_view_children": False,
"tenant_id": tenant_id
}
# Check if this is a child tenant and user has parent access
hierarchy_info = await self._get_tenant_hierarchy(tenant_id)
if hierarchy_info and hierarchy_info.get("parent_tenant_id"):
parent_tenant_id = hierarchy_info["parent_tenant_id"]
parent_access = await self._check_parent_access(user_id, parent_tenant_id)
if parent_access:
user_role = await self.get_user_role_in_tenant(user_id, parent_tenant_id)
# Network admins have full access across entire hierarchy
if user_role == "network_admin":
return {
"access_type": "hierarchical",
"has_access": True,
"tenant_id": tenant_id,
"parent_tenant_id": parent_tenant_id,
"is_network_admin": True,
"can_view_children": True
}
# Regular admins have read-only access to children aggregated data
elif user_role in ["owner", "admin"]:
return {
"access_type": "hierarchical",
"has_access": True,
"tenant_id": tenant_id,
"parent_tenant_id": parent_tenant_id,
"is_network_admin": False,
"can_view_children": True # Can view aggregated data, not detailed
}
return {
"access_type": "none",
"has_access": False,
"tenant_id": tenant_id,
"can_view_children": False
}
async def _check_direct_access(self, user_id: str, tenant_id: str) -> bool:
"""
Check direct access to tenant (without hierarchy)
"""
try:
async with httpx.AsyncClient(timeout=2.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/access/{user_id}"
)
return response.status_code == 200
except Exception as e:
logger.error(f"Failed to check direct access: {e}")
return False
async def _get_tenant_hierarchy(self, tenant_id: str) -> dict:
"""
Get tenant hierarchy information
Args:
tenant_id: Tenant ID
Returns:
dict: Hierarchy information
"""
try:
async with httpx.AsyncClient(timeout=3.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/{tenant_id}/hierarchy"
)
if response.status_code == 200:
return response.json()
return {}
except Exception as e:
logger.error(f"Failed to get tenant hierarchy: {e}")
return {}
async def get_accessible_tenants_hierarchy(self, user_id: str) -> list:
"""
Get all tenants a user has access to, organized in hierarchy
Args:
user_id: User ID
Returns:
list: List of tenants with hierarchy structure
"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/users/{user_id}/hierarchy"
)
if response.status_code == 200:
tenants = response.json()
logger.debug(f"Retrieved user tenants with hierarchy",
user_id=user_id,
tenant_count=len(tenants))
return tenants
else:
logger.warning(f"Failed to get user tenants hierarchy: {response.status_code}")
return []
except Exception as e:
logger.error(f"Failed to get user tenants hierarchy: {e}")
return []
async def get_user_role_in_tenant(self, user_id: str, tenant_id: str) -> Optional[str]:
"""
Get user's role within a specific tenant
Args:
user_id: User ID
tenant_id: Tenant ID
Returns:
Optional[str]: User's role in tenant (owner, admin, manager, user) or None
Optional[str]: User's role in tenant (owner, admin, manager, user, network_admin) or None
"""
try:
async with httpx.AsyncClient(timeout=3.0) as client:
@@ -122,14 +303,14 @@ class TenantAccessManager:
if response.status_code == 200:
data = response.json()
role = data.get("role")
logger.debug(f"User role in tenant",
user_id=user_id,
tenant_id=tenant_id,
logger.debug(f"User role in tenant",
user_id=user_id,
tenant_id=tenant_id,
role=role)
return role
elif response.status_code == 404:
logger.debug(f"User not found in tenant",
user_id=user_id,
logger.debug(f"User not found in tenant",
user_id=user_id,
tenant_id=tenant_id)
return None
else: