Implement row-level security (RLS) context management for database sessions. Refactor invoice processing and reverting tasks to utilize scoped database sessions with RLS context. Update middleware to extract and set company ID from requests. Enhance task dispatching to propagate RLS context via Celery headers. Update architecture documentation to reflect RLS implementation details.
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
"""enable_rls_tenant_company
|
||||
|
||||
Habilita Row-Level Security en las tablas multi-tenant conforme al skill
|
||||
`aduanasoft-dev-standards` (sección 10). Las políticas dependen de dos
|
||||
GUCs que la aplicación establece por transacción con `SET LOCAL`:
|
||||
|
||||
- ``app.tenant_id`` (ID del tenant actual, obligatorio para aislamiento)
|
||||
- ``app.company_id`` (ID de la compañía activa; opcional — si no está fijado
|
||||
la política permite todas las compañías del tenant, útil para vistas de
|
||||
selector de compañía / bootstrap de sesión)
|
||||
|
||||
Las funciones SQL viven en el esquema ``app`` y retornan ``NULL`` cuando la
|
||||
GUC correspondiente está vacía, lo que hace que las comparaciones
|
||||
``col = app.current_xxx_id()`` devuelvan 0 filas sin contexto (fail-closed
|
||||
para ``tenant_id``).
|
||||
|
||||
Las tablas ``core.tenants`` y ``core.user_tenants`` NO quedan bajo RLS: son
|
||||
necesarias para el bootstrap de la sesión (obtener tenant del JWT y listar
|
||||
los tenants del usuario en el selector).
|
||||
|
||||
La migración instala ``FORCE ROW LEVEL SECURITY`` para que las políticas
|
||||
apliquen también al owner — los superusuarios (p. ej. ``postgres`` en dev)
|
||||
siguen haciendo bypass por diseño de PostgreSQL; en producción la API debe
|
||||
conectarse con un rol sin BYPASSRLS.
|
||||
|
||||
Revision ID: d1a2b3c4e5f6
|
||||
Revises: c8d9e0f1a2b3
|
||||
Create Date: 2026-04-24 17:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "d1a2b3c4e5f6"
|
||||
down_revision: Union[str, Sequence[str], None] = "c8d9e0f1a2b3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
TABLES_TENANT_ONLY: list[tuple[str, str]] = [
|
||||
("a76", "company"),
|
||||
("core", "license_usage"),
|
||||
("core", "licenses"),
|
||||
]
|
||||
|
||||
TABLES_TENANT_AND_COMPANY: list[tuple[str, str]] = [
|
||||
("a24", "balance_movement"),
|
||||
("a24", "discharge_detail"),
|
||||
("a24", "discharge_header"),
|
||||
("a24", "discharge_scrap"),
|
||||
("a24", "fa_classes"),
|
||||
("a24", "fa_item_lines"),
|
||||
("a24", "fa_partes"),
|
||||
("a24", "inv_aphis_characteristic"),
|
||||
("a24", "inv_aphis_containers"),
|
||||
("a24", "inv_aphis_entities"),
|
||||
("a24", "inv_aphis_general"),
|
||||
("a24", "inv_aphis_lpcos"),
|
||||
("a24", "inv_aphis_routing"),
|
||||
("a24", "inv_aphis_stype_pitems"),
|
||||
("a24", "inv_bom"),
|
||||
("a24", "inv_classes"),
|
||||
("a24", "inv_parte_paises"),
|
||||
("a24", "inv_partes"),
|
||||
("a76", "app_settings"),
|
||||
("a76", "audit_logs"),
|
||||
("a76", "canadian_tariff_fractions"),
|
||||
("a76", "classes"),
|
||||
("a76", "classification_concepts"),
|
||||
("a76", "clients_and_providers"),
|
||||
("a76", "clients_and_providers_address"),
|
||||
("a76", "clients_and_providers_programs"),
|
||||
("a76", "concept_manifestations"),
|
||||
("a76", "concepts"),
|
||||
("a76", "country_rule_oct"),
|
||||
("a76", "ctm_receipts"),
|
||||
("a76", "customs_broker_concepts"),
|
||||
("a76", "customs_brokers"),
|
||||
("a76", "customs_brokers_personnel"),
|
||||
("a76", "customs_brokers_vu"),
|
||||
("a76", "depreciation_catalog"),
|
||||
("a76", "document_types_digitization"),
|
||||
("a76", "doda"),
|
||||
("a76", "doda_american_pedimentos"),
|
||||
("a76", "doda_container_seals"),
|
||||
("a76", "doda_containers"),
|
||||
("a76", "doda_pedimentos"),
|
||||
("a76", "driver"),
|
||||
("a76", "electronic_notices"),
|
||||
("a76", "equivalencies"),
|
||||
("a76", "equivalency_items"),
|
||||
("a76", "error_catalogs"),
|
||||
("a76", "error_classifications"),
|
||||
("a76", "exchange_rate"),
|
||||
("a76", "fa_location_ext"),
|
||||
("a76", "fda_affirmation_codes"),
|
||||
("a76", "fda_catalog"),
|
||||
("a76", "fda_constituent_elements"),
|
||||
("a76", "fda_lot_production"),
|
||||
("a76", "fda_specifications"),
|
||||
("a76", "fraction_rule_octave"),
|
||||
("a76", "historical_tariff_fractions"),
|
||||
("a76", "identifier_details"),
|
||||
("a76", "identifiers"),
|
||||
("a76", "inpc"),
|
||||
("a76", "invoice_collections"),
|
||||
("a76", "invoice_compliance_mx"),
|
||||
("a76", "invoice_financials"),
|
||||
("a76", "invoice_header"),
|
||||
("a76", "invoice_logistics"),
|
||||
("a76", "invoice_sales_details"),
|
||||
("a76", "invoice_settings"),
|
||||
("a76", "item_line_series"),
|
||||
("a76", "item_lines"),
|
||||
("a76", "item_presets"),
|
||||
("a76", "legends"),
|
||||
("a76", "location"),
|
||||
("a76", "manifest_anexos"),
|
||||
("a76", "manifest_drivers"),
|
||||
("a76", "manifests"),
|
||||
("a76", "multi_currency_types"),
|
||||
("a76", "octave_balance"),
|
||||
("a76", "packages"),
|
||||
("a76", "packing_lists"),
|
||||
("a76", "parts"),
|
||||
("a76", "pedimento_config_additional"),
|
||||
("a76", "pedimento_config_calculations"),
|
||||
("a76", "pedimento_config_parameters"),
|
||||
("a76", "pedimento_config_surcharges"),
|
||||
("a76", "pedimento_config_update_rectification"),
|
||||
("a76", "pedimento_config_updates"),
|
||||
("a76", "pedimento_containers"),
|
||||
("a76", "pedimento_contributions"),
|
||||
("a76", "pedimento_customs_offices"),
|
||||
("a76", "pedimento_dates"),
|
||||
("a76", "pedimento_decrementables"),
|
||||
("a76", "pedimento_guides"),
|
||||
("a76", "pedimento_incrementables"),
|
||||
("a76", "pedimento_indexes"),
|
||||
("a76", "pedimento_packages"),
|
||||
("a76", "pedimento_payments"),
|
||||
("a76", "pedimento_rectification_destination"),
|
||||
("a76", "pedimento_rectification_origin"),
|
||||
("a76", "pedimento_seals"),
|
||||
("a76", "pedimento_transport_carriers"),
|
||||
("a76", "pedimento_transport_means"),
|
||||
("a76", "pedimento_validation"),
|
||||
("a76", "pedimentos"),
|
||||
("a76", "permission_rule_oct"),
|
||||
("a76", "permission_rule_octave"),
|
||||
("a76", "ports"),
|
||||
("a76", "prevalidators"),
|
||||
("a76", "previous_fractions"),
|
||||
("a76", "seal"),
|
||||
("a76", "sectors"),
|
||||
("a76", "signatures"),
|
||||
("a76", "subassembly_entries"),
|
||||
("a76", "trailer"),
|
||||
("a76", "transporter"),
|
||||
("a76", "unit_conversions"),
|
||||
("a76", "units_of_measure"),
|
||||
("a76", "units_of_measure_general"),
|
||||
("a76", "us_tariff_fractions"),
|
||||
("a76", "value_manifestations"),
|
||||
("a76", "vehicle"),
|
||||
("core", "company_roles"),
|
||||
("core", "role_permissions"),
|
||||
("core", "user_company_permissions"),
|
||||
("core", "user_company_roles"),
|
||||
("public", "warning_fractions"),
|
||||
]
|
||||
|
||||
TABLES_COMPANY_ONLY: list[tuple[str, str]] = [
|
||||
("a24", "inv_aphis_catalog"),
|
||||
("a76", "company_address"),
|
||||
("a76", "company_certification"),
|
||||
("a76", "company_cfdi"),
|
||||
("a76", "company_digital_certificate"),
|
||||
("a76", "company_electronic_agent"),
|
||||
("a76", "company_prevalidator"),
|
||||
]
|
||||
|
||||
|
||||
POLICY_TENANT_ONLY = "tenant_isolation"
|
||||
POLICY_TENANT_COMPANY = "tenant_company_isolation"
|
||||
POLICY_COMPANY_ONLY = "company_isolation"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Habilita RLS con políticas de aislamiento por tenant_id / company_id."""
|
||||
op.execute("CREATE SCHEMA IF NOT EXISTS app")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION app.current_tenant_id() RETURNS INTEGER
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT NULLIF(current_setting('app.tenant_id', true), '')::INTEGER
|
||||
$$
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION app.current_company_id() RETURNS INTEGER
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT NULLIF(current_setting('app.company_id', true), '')::INTEGER
|
||||
$$
|
||||
"""
|
||||
)
|
||||
|
||||
for schema, table in TABLES_TENANT_ONLY:
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" ENABLE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" FORCE ROW LEVEL SECURITY')
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE POLICY {POLICY_TENANT_ONLY} ON "{schema}"."{table}"
|
||||
USING (tenant_id = app.current_tenant_id())
|
||||
WITH CHECK (tenant_id = app.current_tenant_id())
|
||||
"""
|
||||
)
|
||||
|
||||
for schema, table in TABLES_TENANT_AND_COMPANY:
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" ENABLE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" FORCE ROW LEVEL SECURITY')
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE POLICY {POLICY_TENANT_COMPANY} ON "{schema}"."{table}"
|
||||
USING (
|
||||
tenant_id = app.current_tenant_id()
|
||||
AND (
|
||||
app.current_company_id() IS NULL
|
||||
OR company_id = app.current_company_id()
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
tenant_id = app.current_tenant_id()
|
||||
AND (
|
||||
app.current_company_id() IS NULL
|
||||
OR company_id = app.current_company_id()
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
for schema, table in TABLES_COMPANY_ONLY:
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" ENABLE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" FORCE ROW LEVEL SECURITY')
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE POLICY {POLICY_COMPANY_ONLY} ON "{schema}"."{table}"
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM a76.company c
|
||||
WHERE c.id = "{schema}"."{table}".company_id
|
||||
AND c.tenant_id = app.current_tenant_id()
|
||||
)
|
||||
AND (
|
||||
app.current_company_id() IS NULL
|
||||
OR company_id = app.current_company_id()
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM a76.company c
|
||||
WHERE c.id = "{schema}"."{table}".company_id
|
||||
AND c.tenant_id = app.current_tenant_id()
|
||||
)
|
||||
AND (
|
||||
app.current_company_id() IS NULL
|
||||
OR company_id = app.current_company_id()
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Revierte: drop policies, deshabilita RLS y elimina helpers."""
|
||||
for schema, table in TABLES_COMPANY_ONLY:
|
||||
op.execute(
|
||||
f'DROP POLICY IF EXISTS {POLICY_COMPANY_ONLY} ON "{schema}"."{table}"'
|
||||
)
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" NO FORCE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" DISABLE ROW LEVEL SECURITY')
|
||||
|
||||
for schema, table in TABLES_TENANT_AND_COMPANY:
|
||||
op.execute(
|
||||
f'DROP POLICY IF EXISTS {POLICY_TENANT_COMPANY} ON "{schema}"."{table}"'
|
||||
)
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" NO FORCE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" DISABLE ROW LEVEL SECURITY')
|
||||
|
||||
for schema, table in TABLES_TENANT_ONLY:
|
||||
op.execute(
|
||||
f'DROP POLICY IF EXISTS {POLICY_TENANT_ONLY} ON "{schema}"."{table}"'
|
||||
)
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" NO FORCE ROW LEVEL SECURITY')
|
||||
op.execute(f'ALTER TABLE "{schema}"."{table}" DISABLE ROW LEVEL SECURITY')
|
||||
|
||||
op.execute("DROP FUNCTION IF EXISTS app.current_company_id()")
|
||||
op.execute("DROP FUNCTION IF EXISTS app.current_tenant_id()")
|
||||
op.execute("DROP SCHEMA IF EXISTS app")
|
||||
Reference in New Issue
Block a user