sprint14-19: ABAC UI rule editor + permission templates + bulk share + analytics + delegation + resolution strategies + migrations 0056-0058

This commit is contained in:
Agent Zero
2026-07-29 02:47:03 +02:00
parent e0003b9384
commit ddf73ee42e
21 changed files with 2399 additions and 4 deletions
+4
View File
@@ -12,6 +12,8 @@ from app.models.contact_folder_permission import ContactFolderPermission
from app.models.contact_merge import ContactMergeHistory
from app.models.entity_permission import EntityPermission
from app.models.entity_policy import EntityPolicy
from app.models.permission_template import PermissionTemplate
from app.models.permission_delegation import PermissionDelegation
from app.models.owned_mixin import OwnedMixin
from app.models.entity_history import EntityHistory
from app.models.currency import Currency
@@ -52,6 +54,8 @@ __all__ = [
"ContactFolderPermission",
"ContactMergeHistory",
"EntityPermission",
"PermissionDelegation",
"PermissionTemplate",
"EntityPolicy",
"OwnedMixin",
"EntityHistory",
+78
View File
@@ -0,0 +1,78 @@
"""Permission delegation model — temporary permission handover between users.
Allows a user to delegate their permissions to another user for a specified
time period and scope.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
CheckConstraint,
DateTime,
ForeignKey,
String,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class PermissionDelegation(Base, TenantMixin):
"""Permission delegation — temporary handover of permissions.
from_user_id delegates their permissions to to_user_id
for the duration [start_at, end_at].
scope: JSONB defining which permissions are delegated.
Examples:
- {"all": true} — all permissions
- {"entity_types": ["contact", "document"]} — specific entity types
- {"permissions": ["contacts:read", "contacts:write"]} — specific permissions
"""
__tablename__ = "permission_delegations"
__table_args__ = (
CheckConstraint(
"end_at > start_at",
name="ck_pd_end_after_start",
),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
from_user_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
to_user_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
start_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
end_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
scope: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)
active: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(),
onupdate=func.now(),
)
+60
View File
@@ -0,0 +1,60 @@
"""Permission template model — reusable permission presets for entity types.
Templates define default sharing rules that can be applied to entities.
When applied, they automatically create entity_permissions entries.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import (
CheckConstraint,
DateTime,
String,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class PermissionTemplate(Base, TenantMixin):
"""Reusable permission template for entity types.
When applied to an entity, the template evaluates trigger_condition
and auto_share_with to create entity_permissions entries.
Fields:
- name: Human-readable template name
- entity_type: Which entity type this template applies to
- trigger_condition: JSONB conditions that must be met for auto-apply
- auto_share_with: JSONB list of {principal_type, principal_id, level} to share with
- level: Default permission level for this template
"""
__tablename__ = "permission_templates"
__table_args__ = (
CheckConstraint(
"level IN ('read', 'write', 'admin', 'delete')",
name="ck_pt_level",
),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(200), nullable=False)
entity_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
trigger_condition: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)
auto_share_with: Mapped[list | None] = mapped_column(JSONB, nullable=True, default=None)
level: Mapped[str] = mapped_column(String(20), nullable=False, default="read")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(),
onupdate=func.now(),
)
+11 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy import CheckConstraint, DateTime, String, func
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -25,6 +25,16 @@ class Tenant(Base):
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
resolution_strategy: Mapped[str] = mapped_column(
String(30), nullable=False, default="highest_wins"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
CheckConstraint(
"resolution_strategy IN ('highest_wins', 'deny_overrides_allow', 'direct_overrides_group', 'most_restrictive_wins')",
name="ck_tenant_resolution_strategy",
),
)