New enterprise feature
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user