abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
198 lines
6.1 KiB
Python
198 lines
6.1 KiB
Python
"""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 UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import 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
|