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
+40 -40
View File
@@ -129,7 +129,7 @@ async def ai_proactive_authed_client(
seed = await seed_tenant_and_users(db_session)
# Grant is_system_admin so require_permission passes for ai_proactive:* permissions
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.id == seed["admin_a"].id).values(is_system_admin=True)
)
@@ -470,7 +470,7 @@ async def test_get_contact_mails_handler(db_session: AsyncSession):
"""get_contact_mails_handler returns mails for a contact."""
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
from app.plugins.builtins.mail.models import Mail, MailAccount, MailFolder
@@ -478,16 +478,16 @@ async def test_get_contact_mails_handler(db_session: AsyncSession):
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="ct@example.com",
name="CT",
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,
@@ -573,7 +573,7 @@ async def test_get_contact_history_handler(db_session: AsyncSession):
"""get_contact_history_handler returns audit log entries."""
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.models.audit import AuditLog
from app.core.auth import hash_password
@@ -581,16 +581,16 @@ async def test_get_contact_history_handler(db_session: AsyncSession):
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="hist@example.com",
name="Hist",
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="Hist",
@@ -631,23 +631,23 @@ async def test_search_related_handler(db_session: AsyncSession):
"""search_related_handler returns similar entities."""
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="Rel Tenant", slug="rel-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="rel@example.com",
name="Rel",
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="Rel",
@@ -677,7 +677,7 @@ async def test_search_related_handler(db_session: AsyncSession):
async def test_summarize_mail_thread_handler(db_session: AsyncSession):
"""summarize_mail_thread_handler returns a summary."""
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.plugins.builtins.mail.models import Mail, MailAccount, MailFolder
from app.core.auth import hash_password
@@ -685,16 +685,16 @@ async def test_summarize_mail_thread_handler(db_session: AsyncSession):
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="thread@example.com",
name="Thread",
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,
@@ -770,23 +770,23 @@ async def test_get_open_tasks_handler(db_session: AsyncSession):
"""get_open_tasks_handler returns open tasks."""
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="Task Tenant", slug="task-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="task@example.com",
name="Task",
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="Task",
@@ -817,7 +817,7 @@ async def test_get_open_tasks_handler(db_session: AsyncSession):
async def test_hybrid_search_handler(db_session: AsyncSession):
"""hybrid_search_handler returns search results."""
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
from app.plugins.builtins.unified_search.provider_registry import (
get_search_registry,
@@ -830,11 +830,9 @@ async def test_hybrid_search_handler(db_session: AsyncSession):
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={},
)
@@ -886,23 +884,25 @@ async def test_gather_context_contact(db_session: AsyncSession):
"""gather_context for contact collects data."""
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="GC Tenant", slug="gc-tenant")
db_session.add(tenant)
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()
user = User(
tenant_id=tenant.id,
email="gc@example.com",
name="GC",
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="GC",
@@ -930,7 +930,7 @@ async def test_gather_context_contact(db_session: AsyncSession):
async def test_gather_context_mail(db_session: AsyncSession):
"""gather_context for mail collects data."""
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user import User, UserTenant
from app.plugins.builtins.mail.models import Mail, MailAccount, MailFolder
from app.core.auth import hash_password
@@ -938,16 +938,16 @@ async def test_gather_context_mail(db_session: AsyncSession):
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="gm@example.com",
name="GM",
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,
@@ -1014,23 +1014,23 @@ async def test_gather_context_company(db_session: AsyncSession):
"""gather_context for company collects data."""
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="GC2 Tenant", slug="gc2-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="gc2@example.com",
name="GC2",
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="GC2 Company",
@@ -1178,23 +1178,23 @@ async def test_handle_context_change_disabled(mock_create_session, redis_client)
async def test_get_active_suggestions_expired(db_session: AsyncSession):
"""Expired suggestions are not returned by get_active_suggestions."""
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="Exp Tenant", slug="exp-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="exp@example.com",
name="Exp",
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()
tenant_id = tenant.id
user_id = user.id
@@ -1239,23 +1239,23 @@ async def test_get_active_suggestions_expired(db_session: AsyncSession):
async def test_execute_suggested_action_success(db_session: AsyncSession):
"""execute_suggested_action executes action and marks suggestion."""
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="ESA Tenant", slug="esa-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="esa@example.com",
name="ESA",
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()
tenant_id = tenant.id
user_id = user.id
@@ -1308,23 +1308,23 @@ async def test_execute_suggested_action_success(db_session: AsyncSession):
async def test_execute_suggested_action_invalid_index(db_session: AsyncSession):
"""execute_suggested_action with invalid index returns error."""
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="InvIdx Tenant", slug="invidx-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="invidx@example.com",
name="InvIdx",
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()
tenant_id = tenant.id
user_id = user.id
@@ -1546,23 +1546,23 @@ async def test_deep_analysis(mock_create_session, db_session: AsyncSession):
from app.plugins.builtins.ai_proactive.jobs import deep_analysis
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="DA Tenant", slug="da-tenant")
db_session.add(tenant)
await db_session.flush()
user = User(
tenant_id=tenant.id,
email="da@example.com",
name="DA",
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="DA",