303 lines
11 KiB
Python
303 lines
11 KiB
Python
"""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")
|