"""Group management service.""" from __future__ import annotations import uuid from typing import Any from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.permissions import invalidate_all_user_permissions from app.models.group import Group, UserGroup from app.models.user import User, UserTenant class GroupService: """Handles group CRUD and user-group membership.""" async def list_groups( self, db: AsyncSession, tenant_id: uuid.UUID, ) -> list[dict[str, Any]]: """List all groups in a tenant.""" q = select(Group).where(Group.tenant_id == tenant_id, Group.deleted_at.is_(None)) result = await db.execute(q) groups = result.scalars().all() return [self._group_to_dict(g) for g in groups] async def get_group( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, ) -> Group | None: """Get a single group by ID within tenant scope.""" q = select(Group).where( Group.id == group_id, Group.tenant_id == tenant_id, Group.deleted_at.is_(None), ) result = await db.execute(q) return result.scalar_one_or_none() async def create_group( self, db: AsyncSession, tenant_id: uuid.UUID, name: str, description: str | None = None, permissions: dict[str, Any] | None = None, denied_permissions: list[str] | None = None, field_permissions: dict[str, Any] | None = None, ) -> Group: """Create a new group.""" group = Group( tenant_id=tenant_id, name=name, description=description, permissions=permissions or {}, denied_permissions=denied_permissions or [], field_permissions=field_permissions or {}, ) db.add(group) await db.flush() return group async def update_group( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, name: str | None = None, description: str | None = None, permissions: dict[str, Any] | None = None, denied_permissions: list[str] | None = None, field_permissions: dict[str, Any] | None = None, ) -> Group | None: """Update a group. Increments permission_version on permission changes.""" q = select(Group).where( Group.id == group_id, Group.tenant_id == tenant_id, Group.deleted_at.is_(None), ) result = await db.execute(q) group = result.scalar_one_or_none() if group is None: return None version_bump = False if name is not None: group.name = name if description is not None: group.description = description if permissions is not None: group.permissions = permissions version_bump = True if denied_permissions is not None: group.denied_permissions = denied_permissions version_bump = True if field_permissions is not None: group.field_permissions = field_permissions version_bump = True if version_bump: group.permission_version += 1 await db.flush() return group async def delete_group( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, ) -> bool: """Soft-delete a group.""" from datetime import UTC, datetime q = select(Group).where( Group.id == group_id, Group.tenant_id == tenant_id, Group.deleted_at.is_(None), ) result = await db.execute(q) group = result.scalar_one_or_none() if group is None: return False group.deleted_at = datetime.now(UTC) await db.flush() return True async def get_group_members( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, ) -> list[dict[str, Any]]: """List all users in a group.""" q = ( select(User, UserGroup) .join(User, UserGroup.user_id == User.id) .where( UserGroup.group_id == group_id, UserGroup.tenant_id == tenant_id, ) ) result = await db.execute(q) return [ { "user_id": str(user.id), "email": user.email, "name": user.name, "is_active": user.is_active, } for user, _ug in result.all() ] async def add_user_to_group( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, user_id: uuid.UUID, ) -> bool: """Add a user to a group.""" # Verify user is in this tenant ut_q = select(UserTenant).where( UserTenant.user_id == user_id, UserTenant.tenant_id == tenant_id, ) ut_result = await db.execute(ut_q) if ut_result.scalar_one_or_none() is None: return False # Check if already member existing_q = select(UserGroup).where( UserGroup.user_id == user_id, UserGroup.group_id == group_id, UserGroup.tenant_id == tenant_id, ) existing_result = await db.execute(existing_q) if existing_result.scalar_one_or_none() is not None: return True # Already member ug = UserGroup( user_id=user_id, group_id=group_id, tenant_id=tenant_id, ) db.add(ug) await db.flush() return True async def remove_user_from_group( self, db: AsyncSession, tenant_id: uuid.UUID, group_id: uuid.UUID, user_id: uuid.UUID, ) -> bool: """Remove a user from a group.""" q = delete(UserGroup).where( UserGroup.user_id == user_id, UserGroup.group_id == group_id, UserGroup.tenant_id == tenant_id, ) result = await db.execute(q) await db.flush() return result.rowcount > 0 async def get_user_groups( self, db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, ) -> list[dict[str, Any]]: """List all groups a user is member of.""" q = ( select(Group) .join(UserGroup, UserGroup.group_id == Group.id) .where( UserGroup.user_id == user_id, UserGroup.tenant_id == tenant_id, Group.deleted_at.is_(None), ) ) result = await db.execute(q) return [self._group_to_dict(g) for g in result.scalars().all()] def _group_to_dict(self, group: Group) -> dict[str, Any]: """Convert group to response dict.""" 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, } group_service = GroupService()