246 lines
7.3 KiB
Python
246 lines
7.3 KiB
Python
"""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()
|