"""Group management routes.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.audit import log_audit from app.core.auth import get_redis from app.core.db import get_db from app.core.permissions import invalidate_all_user_permissions from app.deps import get_current_user, require_permission from app.schemas.group import AddMemberRequest, GroupCreate, GroupUpdate from app.services.group_service import group_service router = APIRouter(prefix="/api/v1/groups", tags=["groups"]) @router.get("") async def list_groups( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all groups in the current tenant.""" import uuid as _uuid tenant_id = _uuid.UUID(current_user["tenant_id"]) return {"items": await group_service.list_groups(db, tenant_id)} @router.post("", status_code=status.HTTP_201_CREATED) async def create_group( body: GroupCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("groups:write")), ): """Create a new group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) acting_user_id = uuid.UUID(current_user["user_id"]) group = await group_service.create_group( db, tenant_id, body.name, body.description, body.permissions, body.denied_permissions, body.field_permissions, ) await log_audit( db, tenant_id, acting_user_id, "create", "group", group.id, changes={"name": body.name}, ) return { "id": str(group.id), "name": group.name, "description": group.description, "permissions": group.permissions, "denied_permissions": group.denied_permissions, "field_permissions": group.field_permissions, "permission_version": group.permission_version, } @router.get("/{group_id}") async def get_group( group_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get a single group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: gid = uuid.UUID(group_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid group_id", "code": "invalid_id"}) from None group = await group_service.get_group(db, tenant_id, gid) if group is None: raise HTTPException(404, detail={"detail": "Group not found", "code": "not_found"}) return { "id": str(group.id), "name": group.name, "description": group.description, "permissions": group.permissions, "denied_permissions": group.denied_permissions, "field_permissions": group.field_permissions, "permission_version": group.permission_version, } @router.patch("/{group_id}") async def update_group( group_id: str, body: GroupUpdate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("groups:write")), ): """Update a group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) acting_user_id = uuid.UUID(current_user["user_id"]) try: gid = uuid.UUID(group_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid group_id", "code": "invalid_id"}) from None group = await group_service.update_group( db, tenant_id, gid, body.name, body.description, body.permissions, body.denied_permissions, body.field_permissions, ) if group is None: raise HTTPException(404, detail={"detail": "Group not found", "code": "not_found"}) # Invalidate permission cache for all users in this tenant redis = get_redis() await invalidate_all_user_permissions(redis, tenant_id) await log_audit( db, tenant_id, acting_user_id, "update", "group", gid, changes=body.model_dump(exclude_none=True), ) return { "id": str(group.id), "name": group.name, "description": group.description, "permissions": group.permissions, "denied_permissions": group.denied_permissions, "field_permissions": group.field_permissions, "permission_version": group.permission_version, } @router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_group( group_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("groups:delete")), ): """Delete a group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) acting_user_id = uuid.UUID(current_user["user_id"]) try: gid = uuid.UUID(group_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid group_id", "code": "invalid_id"}) from None success = await group_service.delete_group(db, tenant_id, gid) if not success: raise HTTPException(404, detail={"detail": "Group not found", "code": "not_found"}) # Invalidate permission cache redis = get_redis() await invalidate_all_user_permissions(redis, tenant_id) await log_audit( db, tenant_id, acting_user_id, "delete", "group", gid, ) @router.get("/{group_id}/members") async def list_group_members( group_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all members of a group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: gid = uuid.UUID(group_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid group_id", "code": "invalid_id"}) from None members = await group_service.get_group_members(db, tenant_id, gid) return {"items": members} @router.post("/{group_id}/members") async def add_group_member( group_id: str, body: AddMemberRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("groups:write")), ): """Add a user to a group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) acting_user_id = uuid.UUID(current_user["user_id"]) try: gid = uuid.UUID(group_id) uid = uuid.UUID(body.user_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid ID", "code": "invalid_id"}) from None success = await group_service.add_user_to_group(db, tenant_id, gid, uid) if not success: raise HTTPException(404, detail={"detail": "Group or user not found in tenant", "code": "not_found"}) # Invalidate permission cache for this user redis = get_redis() from app.core.permissions import invalidate_permission_cache await invalidate_permission_cache(redis, uid, tenant_id) await log_audit( db, tenant_id, acting_user_id, "add_member", "group", gid, changes={"user_id": body.user_id}, ) return {"message": "User added to group"} @router.delete("/{group_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_group_member( group_id: str, user_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("groups:write")), ): """Remove a user from a group.""" tenant_id = uuid.UUID(current_user["tenant_id"]) acting_user_id = uuid.UUID(current_user["user_id"]) try: gid = uuid.UUID(group_id) uid = uuid.UUID(user_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid ID", "code": "invalid_id"}) from None success = await group_service.remove_user_from_group(db, tenant_id, gid, uid) if not success: raise HTTPException(404, detail={"detail": "Membership not found", "code": "not_found"}) # Invalidate permission cache for this user redis = get_redis() from app.core.permissions import invalidate_permission_cache await invalidate_permission_cache(redis, uid, tenant_id) await log_audit( db, tenant_id, acting_user_id, "remove_member", "group", gid, changes={"user_id": user_id}, ) @router.get("/user/{user_id}") async def get_user_groups( user_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List all groups a user is member of.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: uid = uuid.UUID(user_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid user_id", "code": "invalid_id"}) from None groups = await group_service.get_user_groups(db, tenant_id, uid) return {"items": groups}