Security fixes: P0-P2 complete (22 fixes)

P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed
P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK
P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal

8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
This commit is contained in:
Agent Zero
2026-07-25 21:03:46 +02:00
parent aaa7406929
commit 727d86614e
103 changed files with 6831 additions and 1053 deletions
+19 -14
View File
@@ -6,36 +6,28 @@ import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, String, UniqueConstraint, func
from sqlalchemy import Boolean, DateTime, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.core.db import Base, SoftDeleteMixin, TimestampMixin
class User(Base, TenantMixin):
"""User entity — belongs to a tenant, can be member of multiple tenants."""
class User(Base, TimestampMixin, SoftDeleteMixin):
"""User entity — globally unique email, tenant membership via UserTenant."""
__tablename__ = "users"
__table_args__ = (UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"),)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
first_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
last_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(50), nullable=False, default="viewer")
role_id: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("roles.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
preferences: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
is_system_admin: Mapped[bool] = mapped_column(
@@ -44,7 +36,13 @@ class User(Base, TenantMixin):
class UserTenant(Base):
"""N:M association — user membership in tenants."""
"""N:M association — user membership in tenants.
Single source of truth for tenant membership and role assignment.
``role`` is a built-in role string (admin/editor/viewer).
``role_id`` links to a custom Role record for granular RBAC.
``status`` tracks membership lifecycle (active/invited/disabled).
"""
__tablename__ = "user_tenants"
@@ -55,12 +53,19 @@ class UserTenant(Base):
PGUUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), primary_key=True
)
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
role: Mapped[str] = mapped_column(String(50), nullable=False, default="viewer")
role_id: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("roles.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="active", server_default="active"
)
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()
)