diff --git a/alembic/versions/0088_auth_rls_policies.py b/alembic/versions/0088_auth_rls_policies.py new file mode 100644 index 0000000..4f4a4ba --- /dev/null +++ b/alembic/versions/0088_auth_rls_policies.py @@ -0,0 +1,99 @@ +"""Auth RLS policies for password_reset_tokens and audit_log. + +Allows crm_auth to: +- SELECT/UPDATE/INSERT on password_reset_tokens (for password reset flow) +- INSERT on audit_log (for audit logging during auth) +- UPDATE on users (for password hash update during reset) + +The tenant_isolation policy for crm_api/crm_worker is preserved. +crm_auth gets scoped access without full tenant context for token lookup, +but INSERT/UPDATE on tenant tables still requires tenant context. + +Revision ID: 0088 +Revises: 0087 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0088" +down_revision = "0087" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ── password_reset_tokens: replace policy for crm_auth access ── + op.execute("DROP POLICY IF EXISTS password_reset_tokens_tenant_isolation ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_lookup ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_update ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_insert ON public.password_reset_tokens") + + # crm_auth: SELECT without tenant context (token lookup) + op.execute(""" + CREATE POLICY password_reset_tokens_auth_lookup + ON public.password_reset_tokens + FOR SELECT TO crm_auth + USING (true) + """) + + # crm_auth: UPDATE without tenant context (mark token used) + op.execute(""" + CREATE POLICY password_reset_tokens_auth_update + ON public.password_reset_tokens + FOR UPDATE TO crm_auth + USING (true) + WITH CHECK (true) + """) + + # crm_auth: INSERT with tenant context (create new token) + op.execute(""" + CREATE POLICY password_reset_tokens_auth_insert + ON public.password_reset_tokens + FOR INSERT TO crm_auth + WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + """) + + # crm_api, crm_worker: full tenant isolation + op.execute(""" + CREATE POLICY password_reset_tokens_tenant_isolation + ON public.password_reset_tokens + FOR ALL TO crm_api, crm_worker + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + """) + + # ── Grants for crm_auth ── + op.execute("GRANT SELECT, INSERT, UPDATE ON public.password_reset_tokens TO crm_auth") + op.execute("GRANT UPDATE ON public.users TO crm_auth") + + # ── audit_log: allow crm_auth INSERT with tenant context ── + op.execute("DROP POLICY IF EXISTS audit_log_auth_insert ON public.audit_log") + op.execute(""" + CREATE POLICY audit_log_auth_insert + ON public.audit_log + FOR INSERT TO crm_auth + WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + """) + op.execute("GRANT INSERT ON public.audit_log TO crm_auth") + + +def downgrade() -> None: + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_lookup ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_update ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS password_reset_tokens_auth_insert ON public.password_reset_tokens") + op.execute("DROP POLICY IF EXISTS audit_log_auth_insert ON public.audit_log") + op.execute("REVOKE SELECT, INSERT, UPDATE ON public.password_reset_tokens FROM crm_auth") + op.execute("REVOKE UPDATE ON public.users FROM crm_auth") + op.execute("REVOKE INSERT ON public.audit_log FROM crm_auth") + + # Restore original tenant isolation policy + op.execute(""" + CREATE POLICY password_reset_tokens_tenant_isolation + ON public.password_reset_tokens + FOR ALL TO crm_api, crm_worker, crm_auth + USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid) + """)