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:
+40
-40
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user