LightRAG/lightrag/api/routers/membership_routes.py
2025-12-05 14:31:13 +08:00

365 lines
11 KiB
Python

"""
API routes for tenant membership management.
Handles user invitations, role management, and member listing.
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from lightrag.api.models import (
AddMemberRequest,
UpdateMemberRoleRequest,
MemberResponse,
PaginatedMembersResponse,
UserRole,
)
from lightrag.api.dependencies import get_tenant_context_no_kb
from lightrag.models.tenant import TenantContext
from lightrag.api.utils_api import get_combined_auth_dependency
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["membership"])
@router.post(
"/tenants/{tenant_id}/members",
response_model=MemberResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(get_combined_auth_dependency())],
)
async def add_member_to_tenant(
tenant_id: str,
request: AddMemberRequest,
context: TenantContext = Depends(get_tenant_context_no_kb),
):
"""
Add a user to a tenant with specified role.
Requires admin or owner role in the tenant.
"""
from starlette.requests import Request
# Get tenant service from app state
try:
# Access via starlette request
import inspect
frame = inspect.currentframe()
while frame:
if "request" in frame.f_locals and isinstance(
frame.f_locals["request"], Request
):
req = frame.f_locals["request"]
break
frame = frame.f_back
if not req or not hasattr(req.app.state, "rag_manager"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Service not initialized",
)
tenant_service = req.app.state.rag_manager.tenant_service
# Verify requester has admin or owner role
has_permission = await tenant_service.verify_user_access(
user_id=context.user_id, tenant_id=tenant_id, required_role="admin"
)
if not has_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions. Admin or owner role required.",
)
# Add member
membership = await tenant_service.add_user_to_tenant(
user_id=request.user_id,
tenant_id=tenant_id,
role=request.role.value,
created_by=context.user_id,
)
return MemberResponse(
user_id=membership["user_id"],
role=UserRole(membership["role"]),
created_at=membership["created_at"],
created_by=membership["created_by"],
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error adding member to tenant: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@router.get(
"/tenants/{tenant_id}/members",
response_model=PaginatedMembersResponse,
dependencies=[Depends(get_combined_auth_dependency())],
)
async def list_tenant_members(
tenant_id: str,
context: TenantContext = Depends(get_tenant_context_no_kb),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
"""
List all members of a tenant.
Requires at least viewer role in the tenant.
"""
from starlette.requests import Request
try:
# Get tenant service
import inspect
frame = inspect.currentframe()
while frame:
if "request" in frame.f_locals and isinstance(
frame.f_locals["request"], Request
):
req = frame.f_locals["request"]
break
frame = frame.f_back
if not req or not hasattr(req.app.state, "rag_manager"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Service not initialized",
)
tenant_service = req.app.state.rag_manager.tenant_service
# Verify requester has access
has_access = await tenant_service.verify_user_access(
user_id=context.user_id, tenant_id=tenant_id, required_role="viewer"
)
if not has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to tenant"
)
# Get members
result = await tenant_service.get_tenant_members(
tenant_id=tenant_id, skip=skip, limit=limit
)
members = [
MemberResponse(
user_id=m["user_id"],
role=UserRole(m["role"]),
created_at=m["created_at"],
created_by=m["created_by"],
)
for m in result["items"]
]
return PaginatedMembersResponse(
items=members, total=result["total"], skip=skip, limit=limit
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing tenant members: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@router.put(
"/tenants/{tenant_id}/members/{user_id}",
response_model=MemberResponse,
dependencies=[Depends(get_combined_auth_dependency())],
)
async def update_member_role(
tenant_id: str,
user_id: str,
request: UpdateMemberRoleRequest,
context: TenantContext = Depends(get_tenant_context_no_kb),
):
"""
Update a member's role in a tenant.
Requires admin or owner role in the tenant.
"""
from starlette.requests import Request
try:
# Get tenant service
import inspect
frame = inspect.currentframe()
while frame:
if "request" in frame.f_locals and isinstance(
frame.f_locals["request"], Request
):
req = frame.f_locals["request"]
break
frame = frame.f_back
if not req or not hasattr(req.app.state, "rag_manager"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Service not initialized",
)
tenant_service = req.app.state.rag_manager.tenant_service
# Verify requester has admin or owner role
has_permission = await tenant_service.verify_user_access(
user_id=context.user_id, tenant_id=tenant_id, required_role="admin"
)
if not has_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions. Admin or owner role required.",
)
# Update role
membership = await tenant_service.update_user_role(
user_id=user_id, tenant_id=tenant_id, new_role=request.role.value
)
return MemberResponse(
user_id=membership["user_id"],
role=UserRole(membership["role"]),
created_at=membership["created_at"],
created_by=membership["created_by"],
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating member role: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@router.delete(
"/tenants/{tenant_id}/members/{user_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(get_combined_auth_dependency())],
)
async def remove_member_from_tenant(
tenant_id: str,
user_id: str,
context: TenantContext = Depends(get_tenant_context_no_kb),
):
"""
Remove a member from a tenant.
Requires admin or owner role in the tenant.
"""
from starlette.requests import Request
try:
# Get tenant service
import inspect
frame = inspect.currentframe()
while frame:
if "request" in frame.f_locals and isinstance(
frame.f_locals["request"], Request
):
req = frame.f_locals["request"]
break
frame = frame.f_back
if not req or not hasattr(req.app.state, "rag_manager"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Service not initialized",
)
tenant_service = req.app.state.rag_manager.tenant_service
# Verify requester has admin or owner role
has_permission = await tenant_service.verify_user_access(
user_id=context.user_id, tenant_id=tenant_id, required_role="admin"
)
if not has_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions. Admin or owner role required.",
)
# Remove member
removed = await tenant_service.remove_user_from_tenant(
user_id=user_id, tenant_id=tenant_id
)
if not removed:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {user_id} is not a member of tenant {tenant_id}",
)
return None
except HTTPException:
raise
except Exception as e:
logger.error(f"Error removing member from tenant: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@router.get("/users/me/tenants", dependencies=[Depends(get_combined_auth_dependency())])
async def get_my_tenants(
context: TenantContext = Depends(get_tenant_context_no_kb),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
"""
Get all tenants the current user has access to.
"""
from starlette.requests import Request
try:
# Get tenant service
import inspect
frame = inspect.currentframe()
while frame:
if "request" in frame.f_locals and isinstance(
frame.f_locals["request"], Request
):
req = frame.f_locals["request"]
break
frame = frame.f_back
if not req or not hasattr(req.app.state, "rag_manager"):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Service not initialized",
)
tenant_service = req.app.state.rag_manager.tenant_service
# Get user's tenants
result = await tenant_service.get_user_tenants(
user_id=context.user_id, skip=skip, limit=limit
)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting user tenants: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)