feat(01-01): add Alembic migration 027 for config snapshot tables with RLS
- Create router_config_snapshots table with Transit ciphertext storage - Create router_config_diffs table with snapshot pair FK references - Create router_config_changes table for parsed semantic changes - Add RLS tenant isolation (ENABLE + FORCE + USING + WITH CHECK) on all 3 - Add GRANT SELECT/INSERT/DELETE to app_user on all 3 - Add performance indexes: device+collected_at, device+hash, snapshot pair, diff_id Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
129
backend/alembic/versions/027_router_config_snapshots.py
Normal file
129
backend/alembic/versions/027_router_config_snapshots.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Add router config snapshot, diff, and change tables.
|
||||
|
||||
Creates three tables for config snapshot storage:
|
||||
- router_config_snapshots: point-in-time config captures (Transit-encrypted)
|
||||
- router_config_diffs: unified diffs between consecutive snapshots
|
||||
- router_config_changes: parsed semantic changes from diffs
|
||||
|
||||
All tables have RLS tenant isolation and performance indexes.
|
||||
|
||||
Revision ID: 027
|
||||
Revises: 026
|
||||
Create Date: 2026-03-12
|
||||
"""
|
||||
|
||||
revision = "027"
|
||||
down_revision = "026"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# ── router_config_snapshots ──────────────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE router_config_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
config_text TEXT NOT NULL,
|
||||
sha256_hash VARCHAR(64) NOT NULL,
|
||||
collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
|
||||
# RLS
|
||||
conn.execute(sa.text("ALTER TABLE router_config_snapshots ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE router_config_snapshots FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
CREATE POLICY tenant_isolation ON router_config_snapshots
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
|
||||
# Grants
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, DELETE ON router_config_snapshots TO app_user"))
|
||||
|
||||
# Indexes
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX idx_rcs_device_collected ON router_config_snapshots (device_id, collected_at DESC)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX idx_rcs_device_hash ON router_config_snapshots (device_id, sha256_hash)"
|
||||
))
|
||||
|
||||
# ── router_config_diffs ──────────────────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE router_config_diffs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
old_snapshot_id UUID NOT NULL REFERENCES router_config_snapshots(id) ON DELETE CASCADE,
|
||||
new_snapshot_id UUID NOT NULL REFERENCES router_config_snapshots(id) ON DELETE CASCADE,
|
||||
diff_text TEXT NOT NULL,
|
||||
lines_added INT NOT NULL DEFAULT 0,
|
||||
lines_removed INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
|
||||
# RLS
|
||||
conn.execute(sa.text("ALTER TABLE router_config_diffs ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE router_config_diffs FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
CREATE POLICY tenant_isolation ON router_config_diffs
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
|
||||
# Grants
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, DELETE ON router_config_diffs TO app_user"))
|
||||
|
||||
# Indexes
|
||||
conn.execute(sa.text(
|
||||
"CREATE UNIQUE INDEX idx_rcd_snapshot_pair ON router_config_diffs (old_snapshot_id, new_snapshot_id)"
|
||||
))
|
||||
|
||||
# ── router_config_changes ────────────────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE router_config_changes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
diff_id UUID NOT NULL REFERENCES router_config_diffs(id) ON DELETE CASCADE,
|
||||
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
component TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
raw_line TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
|
||||
# RLS
|
||||
conn.execute(sa.text("ALTER TABLE router_config_changes ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE router_config_changes FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
CREATE POLICY tenant_isolation ON router_config_changes
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
|
||||
# Grants
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, DELETE ON router_config_changes TO app_user"))
|
||||
|
||||
# Indexes
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX idx_rcc_diff_id ON router_config_changes (diff_id)"
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(sa.text("DROP TABLE IF EXISTS router_config_changes"))
|
||||
conn.execute(sa.text("DROP TABLE IF EXISTS router_config_diffs"))
|
||||
conn.execute(sa.text("DROP TABLE IF EXISTS router_config_snapshots"))
|
||||
Reference in New Issue
Block a user