phase1: separate DB roles, RLS restoration, login on crm_auth
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- config.py: add auth_database_url, worker_database_url, migration_database_url - db/__init__.py: separate engines for auth/worker/migration + get_auth_db/get_worker_db - auth.py: all auth endpoints use get_auth_db (crm_auth role) - auth_service.py: remove login fallback, require active membership, check status - auth_service.py: switch_tenant checks active membership status - alembic/env.py: use migration_database_url for Alembic - docker-compose.yml: add AUTH_DATABASE_URL, WORKER_DATABASE_URL - .env.example: add all 4 DB URLs with separate roles - migration 0085: transfer ownership to crm_migration, fix BYPASSRLS, enable RLS+FORCE on all tenant tables, drop old policies, create new fail-closed policies scoped to crm_api+crm_worker, revoke excessive grants, grant minimal crm_auth access, drop crm_runtime, set default privileges - tests/test_rls_coverage.py: automated RLS coverage check (13 tests) - tests/test_cross_tenant_security_v2.py: RLS tests with unprivileged role
This commit is contained in:
@@ -63,7 +63,10 @@ class AuthService:
|
||||
return None
|
||||
|
||||
# Get user's default tenant or the one matching slug
|
||||
ut_q = select(UserTenant).where(UserTenant.user_id == user.id)
|
||||
ut_q = select(UserTenant).where(
|
||||
UserTenant.user_id == user.id,
|
||||
UserTenant.status == "active",
|
||||
)
|
||||
if tenant_slug:
|
||||
ut_q = ut_q.join(Tenant, UserTenant.tenant_id == Tenant.id).where(
|
||||
Tenant.slug == tenant_slug
|
||||
@@ -73,12 +76,27 @@ class AuthService:
|
||||
ut_result = await db.execute(ut_q)
|
||||
user_tenant = ut_result.scalar_one_or_none()
|
||||
|
||||
# Fallback: just get first tenant membership
|
||||
# No fallback — if tenant_slug was provided, the membership must exist
|
||||
# and be active in exactly that tenant. If no slug, the default membership
|
||||
# must exist and be active.
|
||||
if user_tenant is None:
|
||||
ut_q2 = select(UserTenant).where(UserTenant.user_id == user.id)
|
||||
if tenant_slug:
|
||||
# Specific tenant requested but no active membership — fail
|
||||
return None
|
||||
# No default membership — check if there are multiple active memberships
|
||||
ut_q2 = select(UserTenant).where(
|
||||
UserTenant.user_id == user.id,
|
||||
UserTenant.status == "active",
|
||||
)
|
||||
ut_result2 = await db.execute(ut_q2)
|
||||
user_tenant = ut_result2.scalar_one_or_none()
|
||||
if user_tenant is None:
|
||||
active_memberships = ut_result2.scalars().all()
|
||||
if len(active_memberships) == 1:
|
||||
# Exactly one active membership — use it
|
||||
user_tenant = active_memberships[0]
|
||||
elif len(active_memberships) == 0:
|
||||
return None
|
||||
else:
|
||||
# Multiple active memberships without a default — must specify tenant_slug
|
||||
return None
|
||||
|
||||
tenant_q = select(Tenant).where(Tenant.id == user_tenant.tenant_id)
|
||||
@@ -152,10 +170,11 @@ class AuthService:
|
||||
|
||||
user_id = uuid.UUID(session_data["user_id"])
|
||||
|
||||
# Verify user is member of target tenant
|
||||
# Verify user has an ACTIVE membership in target tenant
|
||||
ut_q = select(UserTenant).where(
|
||||
UserTenant.user_id == user_id,
|
||||
UserTenant.tenant_id == new_tenant_id,
|
||||
UserTenant.status == "active",
|
||||
)
|
||||
ut_result = await db.execute(ut_q)
|
||||
user_tenant = ut_result.scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user