Files
JQC_multi_tenant/migrations/versions/phase51_external_inspector.py

65 lines
2.5 KiB
Python

"""phase51 — add 'external_inspector' role to users.role ENUM
Introduces the External Inspector role: an inspector employed by the customer
or a third party rather than by the tenant. It has exactly the same
capabilities as the internal 'inspector' role and is scoped the same way —
through InspectorAssignment rows, resolved by get_inspector_scope().
Every capability/scoping check that used to test `role == 'inspector'` now
tests membership of User.INSPECTOR_ROLES (exposed as the `is_inspector`
property), so the new role picks up inspector behaviour everywhere without a
per-route allowlist.
This is a pure ENUM expansion (adds a value, removes and migrates nothing), so
the 3-step ENUM protocol does not apply and re-running the same MODIFY is a
no-op — safe to re-run.
MULTI-TENANT NOTE
-----------------
This runs once per tenant database via control/tenant_migrate.py, like every
other script in migrations/versions. It is a metadata-only ALTER on a small
table, but MySQL still takes an exclusive metadata lock for the duration, so
run it during the normal migration window rather than under load.
Notification matrix rows for the new 'external_inspector' column are NOT seeded
here: MATRIX_DEFAULTS mirrors the Inspector column at runtime and is_enabled()
falls back to that default when a row is absent, so an unseeded tenant behaves
exactly like the Inspector column until an admin saves the matrix page.
"""
revision = 'phase51_external_inspector'
down_revision = 'phase50_sched_acknowledged'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
_ENUM_WITH_EXTERNAL = (
"ENUM('admin','director','inspector','project_manager','customer',"
"'auditor','external_inspector')"
)
_ENUM_WITHOUT_EXTERNAL = (
"ENUM('admin','director','inspector','project_manager','customer','auditor')"
)
def upgrade():
# Idempotent: MODIFY to the expanded set is harmless if already applied.
op.execute(sa.text(
f"ALTER TABLE users MODIFY COLUMN role {_ENUM_WITH_EXTERNAL} NOT NULL"
))
def downgrade():
# Reassign any external_inspector rows before contracting the ENUM so no
# account is orphaned. They become internal inspectors, which keeps their
# InspectorAssignment scoping intact — the same contracts still apply.
op.execute(sa.text(
"UPDATE users SET role = 'inspector' WHERE role = 'external_inspector'"
))
op.execute(sa.text(
f"ALTER TABLE users MODIFY COLUMN role {_ENUM_WITHOUT_EXTERNAL} NOT NULL"
))