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
+22 -22
View File
@@ -110,7 +110,7 @@ async def search_authed_client(
seed = await seed_tenant_and_users(db_session)
# Grant is_system_admin to admin user so search:read/search:admin permissions pass
from sqlalchemy import update
from app.models.user import User
from app.models.user import User, UserTenant
await db_session.execute(
update(User)
.where(User.email == "admin@tenanta.com")
@@ -665,23 +665,23 @@ async def test_index_entity_success(db_session: AsyncSession):
"""index_entity stores embedding in DB and returns True."""
from app.models.contact import Contact
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Test Tenant", slug="test-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="test@example.com",
name="Test",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
contact = Contact(
tenant_id=tenant.id,
first_name="John",
@@ -792,23 +792,23 @@ async def test_hybrid_search_with_results(db_session: AsyncSession):
"""hybrid_search returns results when data exists."""
from app.models.contact import Contact
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Test HS", slug="test-hs")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="hs@example.com",
name="HS",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
contact = Contact(
tenant_id=tenant.id,
first_name="Search",
@@ -867,23 +867,23 @@ async def test_index_mails(mock_index_entity, mock_factory, db_session: AsyncSes
"""index_mails calls index_entity for each mail."""
from app.plugins.builtins.mail.models import Mail, MailAccount, MailFolder
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Mail Tenant", slug="mail-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="mail@example.com",
name="Mail",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
account = MailAccount(
tenant_id=tenant.id,
user_id=user.id,
@@ -954,23 +954,23 @@ async def test_index_contact(mock_index_entity, mock_factory, db_session: AsyncS
"""index_contact calls index_entity."""
from app.models.contact import Contact
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Contact Tenant", slug="contact-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="contact@example.com",
name="Contact",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
contact = Contact(
tenant_id=tenant.id,
first_name="Index",
@@ -996,23 +996,23 @@ async def test_index_contact_company_type(mock_index_entity, mock_factory, db_se
"""index_contact calls index_entity for company-type contact."""
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Company Tenant", slug="company-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="company@example.com",
name="Company",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
company = Company(
tenant_id=tenant.id,
name="Index Company",
@@ -1037,23 +1037,23 @@ async def test_reindex(mock_index_entity, mock_factory, db_session: AsyncSession
"""reindex iterates over all entities of a type."""
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Reindex Tenant", slug="reindex-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="reindex@example.com",
name="Reindex",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
company = Company(
tenant_id=tenant.id,
name="Reindex Co",
@@ -1078,23 +1078,23 @@ async def test_embedding_batch(mock_index_entity, mock_factory, db_session: Asyn
"""embedding_batch finds entities without embedding."""
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.core.auth import hash_password
tenant = Tenant(name="Batch Tenant", slug="batch-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="batch@example.com",
name="Batch",
password_hash=hash_password("TestPass123!"),
role="admin",
is_active=True,
preferences={},
)
db_session.add(user)
await db_session.flush()
db_session.add(UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role="admin"))
await db_session.flush()
company = Company(
tenant_id=tenant.id,
name="Batch Co",