sprint14-19: ABAC UI rule editor + permission templates + bulk share + analytics + delegation + resolution strategies + migrations 0056-0058
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user