fix(i-b): Cross-Tenant-Suite 10/10 gruen — echte RLS-Verifikation statt Vakuum-Tests
Root-Causes und Fixes: (1) conftest.py: crm_api-Rolle (NOSUPERUSER NOBYPASSRLS) mit Grants, RLS auf 117 Tenant-Tabellen aktiviert, tenant_isolation-Policies erstellt — vorher liefen Tests als Superuser (RLS bypassed). (2) test_rls_blocks_cross_tenant_insert: asyncpg fuehrt eagerly aus, RLS-Violation kommt direkt bei execute() nicht erst bei flush() — Doppel-Exception-Erwartung durch Message-Assertion ersetzt. (3) test_rls_tenant_a_insert_own_succeeds: 6 NOT NULL numeric Spalten (discount_*) im Raw-INSERT ergaenzt (Model hat Python-Defaults, DB keine server_defaults). (4) seed_data: commit() fuer Cross-Connection-Sichtbarkeit (crm_api verbindet separat) + Teardown-Cleanup gegen Datenlecks. (5) admin_session: ohne conn.begin() — sonst conditional_savepoint und commit() wirkungslos. (6) sees_only_rows x2: UUID/String-Vergleich normalisiert (asyncpg liefert UUID-Objekte). Vorher: 9 von 10 Tests vakuum-trivial gruen (leere DB, Superuser). Nachher: echte RLS-Assertions mit Seed-Daten als unprivilegierte Rolle.
This commit is contained in:
@@ -236,6 +236,94 @@ def db_setup():
|
||||
except Exception as e:
|
||||
print(f"[CONFTEST] Trigger fix FAILED: {e}")
|
||||
|
||||
# Grant crm_api role access + enable RLS + create tenant isolation
|
||||
# policies (Block E / I-C): The Cross-Tenant Security tests connect as
|
||||
# crm_api (NOSUPERUSER, NOBYPASSRLS) to verify RLS enforcement.
|
||||
print("[CONFTEST] Setting up RLS grants and policies...")
|
||||
try:
|
||||
sync_eng3 = _get_sync_engine()
|
||||
with sync_eng3.connect() as conn:
|
||||
# 1. Ensure crm_api role exists
|
||||
conn.execute(text(
|
||||
"DO $$ BEGIN "
|
||||
"IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_api') THEN "
|
||||
"CREATE ROLE crm_api LOGIN PASSWORD 'crm_api_password' NOSUPERUSER NOBYPASSRLS; "
|
||||
"END IF; END $$;"
|
||||
))
|
||||
|
||||
# 2. Enable RLS on all tenant tables EXCEPT auth-critical ones
|
||||
# (user_tenants must be readable without tenant context for login)
|
||||
_no_rls_tables = "('user_tenants','tenants','users','audit_log','alembic_version','groups','user_groups','roles')"
|
||||
conn.execute(text(f"""
|
||||
DO $$ DECLARE r RECORD;
|
||||
BEGIN
|
||||
FOR r IN (
|
||||
SELECT c.relname AS tablename
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND c.relkind = 'r'
|
||||
AND c.relrowsecurity = false
|
||||
AND c.relname NOT IN {_no_rls_tables}
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns ic
|
||||
WHERE ic.table_schema = 'public'
|
||||
AND ic.table_name = c.relname
|
||||
AND ic.column_name = 'tenant_id'
|
||||
)
|
||||
) LOOP
|
||||
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', r.tablename);
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""))
|
||||
|
||||
# 3. Create standard tenant-isolation policy per table that has
|
||||
# RLS enabled but no policy yet
|
||||
conn.execute(text("""
|
||||
DO $$ DECLARE r RECORD;
|
||||
BEGIN
|
||||
FOR r IN (
|
||||
SELECT c.relname AS tablename
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND c.relkind = 'r'
|
||||
AND c.relrowsecurity = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies p
|
||||
WHERE p.schemaname = 'public'
|
||||
AND p.tablename = c.relname
|
||||
)
|
||||
) LOOP
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I_tenant_isolation ON public.%I '
|
||||
'FOR ALL USING (tenant_id = current_setting(''app.current_tenant_id'', true)::uuid) '
|
||||
'WITH CHECK (tenant_id = current_setting(''app.current_tenant_id'', true)::uuid)',
|
||||
r.tablename, r.tablename
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""))
|
||||
|
||||
# 4. Grant crm_api access to all tables
|
||||
conn.execute(text("GRANT USAGE ON SCHEMA public TO crm_api"))
|
||||
conn.execute(text("GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO crm_api"))
|
||||
conn.execute(text("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_api"))
|
||||
conn.commit()
|
||||
|
||||
# Count results
|
||||
with sync_eng3.connect() as verify_conn:
|
||||
rls_count = verify_conn.execute(text(
|
||||
"SELECT count(*) FROM pg_tables WHERE schemaname='public' AND rowsecurity=true"
|
||||
)).scalar()
|
||||
pol_count = verify_conn.execute(text(
|
||||
"SELECT count(*) FROM pg_policies WHERE schemaname='public'"
|
||||
)).scalar()
|
||||
print(f"[CONFTEST] RLS setup complete: {rls_count} RLS tables, {pol_count} policies")
|
||||
sync_eng3.dispose()
|
||||
except Exception as e:
|
||||
print(f"[CONFTEST] RLS setup FAILED: {e}")
|
||||
|
||||
|
||||
yield
|
||||
|
||||
|
||||
Reference in New Issue
Block a user