Files
leocrm/app/services/delegation_service.py
T

198 lines
6.1 KiB
Python
Raw Normal View History

"""Permission delegation service — CRUD + active check for permission handovers.
Allows users to temporarily delegate their permissions to other users
for a specified time period and scope.
"""
from __future__ import annotations
import logging
import uuid
from datetime import datetime, UTC
from typing import Any
from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.permission_delegation import PermissionDelegation
logger = logging.getLogger(__name__)
def _serialize_delegation(d: PermissionDelegation) -> dict:
return {
"id": str(d.id),
"from_user_id": str(d.from_user_id),
"to_user_id": str(d.to_user_id),
"start_at": d.start_at.isoformat() if d.start_at else None,
"end_at": d.end_at.isoformat() if d.end_at else None,
"scope": d.scope,
"active": d.active,
"tenant_id": str(d.tenant_id),
"created_at": d.created_at.isoformat() if d.created_at else None,
"updated_at": d.updated_at.isoformat() if d.updated_at else None,
}
async def list_delegations(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID | None = None,
direction: str = "all",
) -> list[dict]:
"""List delegations for a tenant.
Args:
direction: 'from' (delegations I created), 'to' (delegations to me), 'all' (both)
"""
query = select(PermissionDelegation).where(PermissionDelegation.tenant_id == tenant_id)
if user_id:
if direction == "from":
query = query.where(PermissionDelegation.from_user_id == user_id)
elif direction == "to":
query = query.where(PermissionDelegation.to_user_id == user_id)
else:
query = query.where(
or_(
PermissionDelegation.from_user_id == user_id,
PermissionDelegation.to_user_id == user_id,
)
)
query = query.order_by(PermissionDelegation.created_at.desc())
result = await db.execute(query)
delegations = result.scalars().all()
return [_serialize_delegation(d) for d in delegations]
async def create_delegation(
db: AsyncSession,
tenant_id: uuid.UUID,
from_user_id: uuid.UUID,
to_user_id: uuid.UUID,
start_at: datetime,
end_at: datetime,
scope: dict | None = None,
) -> dict:
"""Create a new permission delegation."""
if end_at <= start_at:
raise ValueError("end_at must be after start_at")
delegation = PermissionDelegation(
tenant_id=tenant_id,
from_user_id=from_user_id,
to_user_id=to_user_id,
start_at=start_at,
end_at=end_at,
scope=scope,
)
db.add(delegation)
await db.commit()
await db.refresh(delegation)
return _serialize_delegation(delegation)
async def update_delegation(
db: AsyncSession,
tenant_id: uuid.UUID,
delegation_id: str,
**kwargs: Any,
) -> dict:
"""Update an existing delegation."""
result = await db.execute(
select(PermissionDelegation)
.where(PermissionDelegation.id == uuid.UUID(delegation_id))
.where(PermissionDelegation.tenant_id == tenant_id)
)
delegation = result.scalar_one_or_none()
if delegation is None:
raise ValueError(f"Delegation {delegation_id} not found")
updatable_fields = {"start_at", "end_at", "scope", "active"}
for key, value in kwargs.items():
if key in updatable_fields and value is not None:
setattr(delegation, key, value)
await db.commit()
await db.refresh(delegation)
return _serialize_delegation(delegation)
async def delete_delegation(
db: AsyncSession,
tenant_id: uuid.UUID,
delegation_id: str,
) -> None:
"""Delete a delegation."""
result = await db.execute(
select(PermissionDelegation)
.where(PermissionDelegation.id == uuid.UUID(delegation_id))
.where(PermissionDelegation.tenant_id == tenant_id)
)
delegation = result.scalar_one_or_none()
if delegation is None:
raise ValueError(f"Delegation {delegation_id} not found")
await db.delete(delegation)
await db.commit()
async def is_delegation_active(
db: AsyncSession,
user_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> bool:
"""Check if a user has any active delegations (as delegatee).
Returns True if there is at least one active delegation where
this user is the to_user_id and the current time is within [start_at, end_at].
"""
now = datetime.now(UTC)
result = await db.execute(
select(PermissionDelegation)
.where(PermissionDelegation.to_user_id == user_id)
.where(PermissionDelegation.tenant_id == tenant_id)
.where(PermissionDelegation.active == True) # noqa: E712
.where(PermissionDelegation.start_at <= now)
.where(PermissionDelegation.end_at > now)
)
delegation = result.scalar_one_or_none()
return delegation is not None
async def get_active_delegations(
db: AsyncSession,
user_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> list[dict]:
"""Get all active delegations for a user (as delegatee)."""
now = datetime.now(UTC)
result = await db.execute(
select(PermissionDelegation)
.where(PermissionDelegation.to_user_id == user_id)
.where(PermissionDelegation.tenant_id == tenant_id)
.where(PermissionDelegation.active == True) # noqa: E712
.where(PermissionDelegation.start_at <= now)
.where(PermissionDelegation.end_at > now)
)
delegations = result.scalars().all()
return [_serialize_delegation(d) for d in delegations]
async def deactivate_expired_delegations(db: AsyncSession) -> int:
"""Deactivate all delegations that have passed their end_at."""
now = datetime.now(UTC)
result = await db.execute(
select(PermissionDelegation)
.where(PermissionDelegation.active == True) # noqa: E712
.where(PermissionDelegation.end_at <= now)
)
expired = result.scalars().all()
count = len(expired)
for delegation in expired:
delegation.active = False
if count > 0:
await db.commit()
logger.info("Deactivated %d expired delegations", count)
return count