fix(lint): resolve all ruff lint errors
Add ruff config to exclude alembic E402, SQLAlchemy F821, and pre-existing E501 line-length issues. Auto-fix 69 unused imports and 2 f-strings without placeholders. Manually fix 8 unused variables. Apply ruff format to 127 files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -220,7 +220,8 @@ def upgrade() -> None:
|
||||
# Super admin sees all; tenant users see only their tenant
|
||||
conn.execute(sa.text("ALTER TABLE tenants ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE tenants FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON tenants
|
||||
USING (
|
||||
id::text = current_setting('app.current_tenant', true)
|
||||
@@ -230,13 +231,15 @@ def upgrade() -> None:
|
||||
id::text = current_setting('app.current_tenant', true)
|
||||
OR current_setting('app.current_tenant', true) = 'super_admin'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- USERS RLS ---
|
||||
# Users see only other users in their tenant; super_admin sees all
|
||||
conn.execute(sa.text("ALTER TABLE users ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE users FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON users
|
||||
USING (
|
||||
tenant_id::text = current_setting('app.current_tenant', true)
|
||||
@@ -246,41 +249,49 @@ def upgrade() -> None:
|
||||
tenant_id::text = current_setting('app.current_tenant', true)
|
||||
OR current_setting('app.current_tenant', true) = 'super_admin'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- DEVICES RLS ---
|
||||
conn.execute(sa.text("ALTER TABLE devices ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE devices FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON devices
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- DEVICE GROUPS RLS ---
|
||||
conn.execute(sa.text("ALTER TABLE device_groups ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE device_groups FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON device_groups
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- DEVICE TAGS RLS ---
|
||||
conn.execute(sa.text("ALTER TABLE device_tags ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE device_tags FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON device_tags
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- DEVICE GROUP MEMBERSHIPS RLS ---
|
||||
# These are filtered by joining through devices/groups (which already have RLS)
|
||||
# But we also add direct RLS via a join to the devices table
|
||||
conn.execute(sa.text("ALTER TABLE device_group_memberships ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE device_group_memberships FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON device_group_memberships
|
||||
USING (
|
||||
EXISTS (
|
||||
@@ -296,12 +307,14 @@ def upgrade() -> None:
|
||||
AND d.tenant_id::text = current_setting('app.current_tenant', true)
|
||||
)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# --- DEVICE TAG ASSIGNMENTS RLS ---
|
||||
conn.execute(sa.text("ALTER TABLE device_tag_assignments ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("ALTER TABLE device_tag_assignments FORCE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON device_tag_assignments
|
||||
USING (
|
||||
EXISTS (
|
||||
@@ -317,7 +330,8 @@ def upgrade() -> None:
|
||||
AND d.tenant_id::text = current_setting('app.current_tenant', true)
|
||||
)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# GRANT PERMISSIONS TO app_user (RLS-enforcing application role)
|
||||
@@ -336,9 +350,7 @@ def upgrade() -> None:
|
||||
]
|
||||
|
||||
for table in tables:
|
||||
conn.execute(sa.text(
|
||||
f"GRANT SELECT, INSERT, UPDATE, DELETE ON {table} TO app_user"
|
||||
))
|
||||
conn.execute(sa.text(f"GRANT SELECT, INSERT, UPDATE, DELETE ON {table} TO app_user"))
|
||||
|
||||
# Grant sequence usage for UUID generation (gen_random_uuid is built-in, but just in case)
|
||||
conn.execute(sa.text("GRANT USAGE ON SCHEMA public TO app_user"))
|
||||
|
||||
@@ -46,7 +46,8 @@ def upgrade() -> None:
|
||||
# to read all devices across all tenants, which is required for polling.
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'poller_user') THEN
|
||||
@@ -54,7 +55,8 @@ def upgrade() -> None:
|
||||
END IF;
|
||||
END
|
||||
$$
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT CONNECT ON DATABASE tod TO poller_user"))
|
||||
conn.execute(sa.text("GRANT USAGE ON SCHEMA public TO poller_user"))
|
||||
|
||||
@@ -34,7 +34,8 @@ def upgrade() -> None:
|
||||
# Stores per-interface byte counters from /interface/print on every poll cycle.
|
||||
# rx_bps/tx_bps are stored as NULL — computed at query time via LAG() window
|
||||
# function to avoid delta state in the poller.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS interface_metrics (
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
device_id UUID NOT NULL,
|
||||
@@ -45,23 +46,28 @@ def upgrade() -> None:
|
||||
rx_bps BIGINT,
|
||||
tx_bps BIGINT
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"SELECT create_hypertable('interface_metrics', 'time', if_not_exists => TRUE)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text("SELECT create_hypertable('interface_metrics', 'time', if_not_exists => TRUE)")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_interface_metrics_device_time "
|
||||
"ON interface_metrics (device_id, time DESC)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_interface_metrics_device_time "
|
||||
"ON interface_metrics (device_id, time DESC)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE interface_metrics ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON interface_metrics
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON interface_metrics TO app_user"))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON interface_metrics TO poller_user"))
|
||||
@@ -72,7 +78,8 @@ def upgrade() -> None:
|
||||
# Stores per-device system health metrics from /system/resource/print and
|
||||
# /system/health/print on every poll cycle.
|
||||
# temperature is nullable — not all RouterOS devices have temperature sensors.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS health_metrics (
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
device_id UUID NOT NULL,
|
||||
@@ -84,23 +91,28 @@ def upgrade() -> None:
|
||||
total_disk BIGINT,
|
||||
temperature SMALLINT
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"SELECT create_hypertable('health_metrics', 'time', if_not_exists => TRUE)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text("SELECT create_hypertable('health_metrics', 'time', if_not_exists => TRUE)")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_health_metrics_device_time "
|
||||
"ON health_metrics (device_id, time DESC)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_health_metrics_device_time "
|
||||
"ON health_metrics (device_id, time DESC)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE health_metrics ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON health_metrics
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON health_metrics TO app_user"))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON health_metrics TO poller_user"))
|
||||
@@ -113,7 +125,8 @@ def upgrade() -> None:
|
||||
# /interface/wifi/registration-table/print (v7).
|
||||
# ccq may be 0 on RouterOS v7 (not available in the WiFi API path).
|
||||
# avg_signal is dBm (negative integer, e.g. -67).
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS wireless_metrics (
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
device_id UUID NOT NULL,
|
||||
@@ -124,23 +137,28 @@ def upgrade() -> None:
|
||||
ccq SMALLINT,
|
||||
frequency INTEGER
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"SELECT create_hypertable('wireless_metrics', 'time', if_not_exists => TRUE)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text("SELECT create_hypertable('wireless_metrics', 'time', if_not_exists => TRUE)")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_wireless_metrics_device_time "
|
||||
"ON wireless_metrics (device_id, time DESC)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_wireless_metrics_device_time "
|
||||
"ON wireless_metrics (device_id, time DESC)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE wireless_metrics ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON wireless_metrics
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON wireless_metrics TO app_user"))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON wireless_metrics TO poller_user"))
|
||||
|
||||
@@ -32,7 +32,8 @@ def upgrade() -> None:
|
||||
# Stores metadata for each backup run. The actual config content lives in
|
||||
# the tenant's bare git repository (GIT_STORE_PATH). This table provides
|
||||
# the timeline view and change tracking without duplicating file content.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS config_backup_runs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
@@ -43,19 +44,24 @@ def upgrade() -> None:
|
||||
lines_removed INT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_backup_runs_device_created "
|
||||
"ON config_backup_runs (device_id, created_at DESC)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_backup_runs_device_created "
|
||||
"ON config_backup_runs (device_id, created_at DESC)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE config_backup_runs ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON config_backup_runs
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON config_backup_runs TO app_user"))
|
||||
conn.execute(sa.text("GRANT SELECT ON config_backup_runs TO poller_user"))
|
||||
@@ -68,7 +74,8 @@ def upgrade() -> None:
|
||||
# A per-device row with a specific device_id overrides the tenant default.
|
||||
# UNIQUE(tenant_id, device_id) allows one entry per (tenant, device) pair
|
||||
# where device_id NULL is the tenant-level default.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS config_backup_schedules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -78,14 +85,17 @@ def upgrade() -> None:
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, device_id)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE config_backup_schedules ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON config_backup_schedules
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE ON config_backup_schedules TO app_user"))
|
||||
|
||||
@@ -97,7 +107,8 @@ def upgrade() -> None:
|
||||
# startup handler checks for 'pending_verification' rows and either verifies
|
||||
# connectivity (clean up the RouterOS scheduler job) or marks as failed.
|
||||
# See Pitfall 6 in 04-RESEARCH.md.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS config_push_operations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
@@ -108,14 +119,17 @@ def upgrade() -> None:
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE config_push_operations ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON config_push_operations
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE ON config_push_operations TO app_user"))
|
||||
|
||||
|
||||
@@ -31,24 +31,27 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# ALTER devices TABLE — add architecture and preferred_channel columns
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN IF NOT EXISTS architecture TEXT"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN IF NOT EXISTS preferred_channel TEXT DEFAULT 'stable' NOT NULL"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN IF NOT EXISTS architecture TEXT"))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN IF NOT EXISTS preferred_channel TEXT DEFAULT 'stable' NOT NULL"
|
||||
)
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ALTER device_groups TABLE — add preferred_channel column
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE device_groups ADD COLUMN IF NOT EXISTS preferred_channel TEXT DEFAULT 'stable' NOT NULL"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"ALTER TABLE device_groups ADD COLUMN IF NOT EXISTS preferred_channel TEXT DEFAULT 'stable' NOT NULL"
|
||||
)
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CREATE alert_rules TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS alert_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -64,27 +67,31 @@ def upgrade() -> None:
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_rules_tenant_enabled "
|
||||
"ON alert_rules (tenant_id, enabled)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_rules_tenant_enabled "
|
||||
"ON alert_rules (tenant_id, enabled)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE alert_rules ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON alert_rules
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON alert_rules TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON alert_rules TO app_user"))
|
||||
conn.execute(sa.text("GRANT ALL ON alert_rules TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE notification_channels TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -100,52 +107,60 @@ def upgrade() -> None:
|
||||
webhook_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_notification_channels_tenant "
|
||||
"ON notification_channels (tenant_id)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_notification_channels_tenant "
|
||||
"ON notification_channels (tenant_id)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE notification_channels ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON notification_channels
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON notification_channels TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON notification_channels TO app_user")
|
||||
)
|
||||
conn.execute(sa.text("GRANT ALL ON notification_channels TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE alert_rule_channels TABLE (M2M association)
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS alert_rule_channels (
|
||||
rule_id UUID NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (rule_id, channel_id)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE alert_rule_channels ENABLE ROW LEVEL SECURITY"))
|
||||
# RLS for M2M: join through parent table's tenant_id via rule_id
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON alert_rule_channels
|
||||
USING (rule_id IN (
|
||||
SELECT id FROM alert_rules
|
||||
WHERE tenant_id::text = current_setting('app.current_tenant')
|
||||
))
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON alert_rule_channels TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON alert_rule_channels TO app_user"))
|
||||
conn.execute(sa.text("GRANT ALL ON alert_rule_channels TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE alert_events TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS alert_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rule_id UUID REFERENCES alert_rules(id) ON DELETE SET NULL,
|
||||
@@ -164,31 +179,37 @@ def upgrade() -> None:
|
||||
fired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_events_device_rule_status "
|
||||
"ON alert_events (device_id, rule_id, status)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_events_tenant_fired "
|
||||
"ON alert_events (tenant_id, fired_at)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_events_device_rule_status "
|
||||
"ON alert_events (device_id, rule_id, status)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_alert_events_tenant_fired "
|
||||
"ON alert_events (tenant_id, fired_at)"
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE alert_events ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON alert_events
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON alert_events TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON alert_events TO app_user"))
|
||||
conn.execute(sa.text("GRANT ALL ON alert_events TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE firmware_versions TABLE (global — NOT tenant-scoped)
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS firmware_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
architecture TEXT NOT NULL,
|
||||
@@ -200,23 +221,25 @@ def upgrade() -> None:
|
||||
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(architecture, channel, version)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_firmware_versions_arch_channel "
|
||||
"ON firmware_versions (architecture, channel)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_firmware_versions_arch_channel "
|
||||
"ON firmware_versions (architecture, channel)"
|
||||
)
|
||||
)
|
||||
|
||||
# No RLS on firmware_versions — global cache table
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE ON firmware_versions TO app_user"
|
||||
))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE ON firmware_versions TO app_user"))
|
||||
conn.execute(sa.text("GRANT ALL ON firmware_versions TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE firmware_upgrade_jobs TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS firmware_upgrade_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -234,16 +257,19 @@ def upgrade() -> None:
|
||||
confirmed_major_upgrade BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
conn.execute(sa.text("ALTER TABLE firmware_upgrade_jobs ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY tenant_isolation ON firmware_upgrade_jobs
|
||||
USING (tenant_id::text = current_setting('app.current_tenant'))
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON firmware_upgrade_jobs TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON firmware_upgrade_jobs TO app_user")
|
||||
)
|
||||
conn.execute(sa.text("GRANT ALL ON firmware_upgrade_jobs TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
@@ -252,21 +278,27 @@ def upgrade() -> None:
|
||||
# Note: New tenant creation (in the tenants API router) should also seed
|
||||
# these three default rules. A _seed_default_alert_rules(tenant_id) helper
|
||||
# should be created in the alerts router or a shared service for this.
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO alert_rules (id, tenant_id, name, metric, operator, threshold, duration_polls, severity, enabled, is_default)
|
||||
SELECT gen_random_uuid(), t.id, 'High CPU Usage', 'cpu_load', 'gt', 90, 5, 'warning', TRUE, TRUE
|
||||
FROM tenants t
|
||||
"""))
|
||||
conn.execute(sa.text("""
|
||||
""")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO alert_rules (id, tenant_id, name, metric, operator, threshold, duration_polls, severity, enabled, is_default)
|
||||
SELECT gen_random_uuid(), t.id, 'High Memory Usage', 'memory_used_pct', 'gt', 90, 5, 'warning', TRUE, TRUE
|
||||
FROM tenants t
|
||||
"""))
|
||||
conn.execute(sa.text("""
|
||||
""")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO alert_rules (id, tenant_id, name, metric, operator, threshold, duration_polls, severity, enabled, is_default)
|
||||
SELECT gen_random_uuid(), t.id, 'High Disk Usage', 'disk_used_pct', 'gt', 85, 3, 'warning', TRUE, TRUE
|
||||
FROM tenants t
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -31,17 +31,14 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# ALTER devices TABLE — add latitude and longitude columns
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN IF NOT EXISTS latitude DOUBLE PRECISION"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN IF NOT EXISTS longitude DOUBLE PRECISION"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN IF NOT EXISTS latitude DOUBLE PRECISION"))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN IF NOT EXISTS longitude DOUBLE PRECISION"))
|
||||
|
||||
# =========================================================================
|
||||
# CREATE config_templates TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS config_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -53,12 +50,14 @@ def upgrade() -> None:
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(tenant_id, name)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CREATE config_template_tags TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS config_template_tags (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -66,12 +65,14 @@ def upgrade() -> None:
|
||||
template_id UUID NOT NULL REFERENCES config_templates(id) ON DELETE CASCADE,
|
||||
UNIQUE(template_id, name)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CREATE template_push_jobs TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS template_push_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -86,48 +87,57 @@ def upgrade() -> None:
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RLS POLICIES
|
||||
# =========================================================================
|
||||
for table in ("config_templates", "config_template_tags", "template_push_jobs"):
|
||||
conn.execute(sa.text(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text(f"""
|
||||
conn.execute(
|
||||
sa.text(f"""
|
||||
CREATE POLICY {table}_tenant_isolation ON {table}
|
||||
USING (tenant_id = current_setting('app.current_tenant')::uuid)
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
f"GRANT SELECT, INSERT, UPDATE, DELETE ON {table} TO app_user"
|
||||
))
|
||||
""")
|
||||
)
|
||||
conn.execute(sa.text(f"GRANT SELECT, INSERT, UPDATE, DELETE ON {table} TO app_user"))
|
||||
conn.execute(sa.text(f"GRANT ALL ON {table} TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# INDEXES
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_templates_tenant "
|
||||
"ON config_templates (tenant_id)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_template_tags_template "
|
||||
"ON config_template_tags (template_id)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_template_push_jobs_tenant_rollout "
|
||||
"ON template_push_jobs (tenant_id, rollout_id)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_template_push_jobs_device_status "
|
||||
"ON template_push_jobs (device_id, status)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_templates_tenant ON config_templates (tenant_id)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_config_template_tags_template "
|
||||
"ON config_template_tags (template_id)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_template_push_jobs_tenant_rollout "
|
||||
"ON template_push_jobs (tenant_id, rollout_id)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_template_push_jobs_device_status "
|
||||
"ON template_push_jobs (device_id, status)"
|
||||
)
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# SEED STARTER TEMPLATES for all existing tenants
|
||||
# =========================================================================
|
||||
|
||||
# 1. Basic Firewall
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -146,10 +156,12 @@ add chain=forward action=drop',
|
||||
'[{"name":"wan_interface","type":"string","default":"ether1","description":"WAN-facing interface"},{"name":"allowed_network","type":"subnet","default":"192.168.1.0/24","description":"Allowed source network"}]'::jsonb
|
||||
FROM tenants t
|
||||
ON CONFLICT DO NOTHING
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 2. DHCP Server Setup
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -162,10 +174,12 @@ add chain=forward action=drop',
|
||||
'[{"name":"pool_start","type":"ip","default":"192.168.1.100","description":"DHCP pool start address"},{"name":"pool_end","type":"ip","default":"192.168.1.254","description":"DHCP pool end address"},{"name":"gateway","type":"ip","default":"192.168.1.1","description":"Default gateway"},{"name":"dns_server","type":"ip","default":"8.8.8.8","description":"DNS server address"},{"name":"interface","type":"string","default":"bridge1","description":"Interface to serve DHCP on"}]'::jsonb
|
||||
FROM tenants t
|
||||
ON CONFLICT DO NOTHING
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 3. Wireless AP Config
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -177,10 +191,12 @@ add chain=forward action=drop',
|
||||
'[{"name":"ssid","type":"string","default":"MikroTik-AP","description":"Wireless network name"},{"name":"password","type":"string","default":"","description":"WPA2 pre-shared key (min 8 characters)"},{"name":"frequency","type":"integer","default":"2412","description":"Wireless frequency in MHz"},{"name":"channel_width","type":"string","default":"20/40mhz-XX","description":"Channel width setting"}]'::jsonb
|
||||
FROM tenants t
|
||||
ON CONFLICT DO NOTHING
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 4. Initial Device Setup
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -196,7 +212,8 @@ add chain=forward action=drop',
|
||||
'[{"name":"ntp_server","type":"ip","default":"pool.ntp.org","description":"NTP server address"},{"name":"dns_servers","type":"string","default":"8.8.8.8,8.8.4.4","description":"Comma-separated DNS servers"}]'::jsonb
|
||||
FROM tenants t
|
||||
ON CONFLICT DO NOTHING
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -29,7 +29,8 @@ def upgrade() -> None:
|
||||
# =========================================================================
|
||||
# CREATE audit_logs TABLE
|
||||
# =========================================================================
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -42,39 +43,40 @@ def upgrade() -> None:
|
||||
ip_address VARCHAR(45),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RLS POLICY
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY"
|
||||
))
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(sa.text("ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE POLICY audit_logs_tenant_isolation ON audit_logs
|
||||
USING (tenant_id = current_setting('app.current_tenant')::uuid)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# Grant SELECT + INSERT to app_user (no UPDATE/DELETE -- audit logs are immutable)
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT ON audit_logs TO app_user"
|
||||
))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT ON audit_logs TO app_user"))
|
||||
# Poller user gets full access for cross-tenant audit logging
|
||||
conn.execute(sa.text(
|
||||
"GRANT ALL ON audit_logs TO poller_user"
|
||||
))
|
||||
conn.execute(sa.text("GRANT ALL ON audit_logs TO poller_user"))
|
||||
|
||||
# =========================================================================
|
||||
# INDEXES
|
||||
# =========================================================================
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created "
|
||||
"ON audit_logs (tenant_id, created_at DESC)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_action "
|
||||
"ON audit_logs (tenant_id, action)"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created "
|
||||
"ON audit_logs (tenant_id, created_at DESC)"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_action "
|
||||
"ON audit_logs (tenant_id, action)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -28,7 +28,8 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# ── 1. Create maintenance_windows table ────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE TABLE IF NOT EXISTS maintenance_windows (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
@@ -44,18 +45,22 @@ def upgrade() -> None:
|
||||
|
||||
CONSTRAINT chk_maintenance_window_dates CHECK (end_at > start_at)
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# ── 2. Composite index for active window queries ───────────────────────
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_maintenance_windows_tenant_time
|
||||
ON maintenance_windows (tenant_id, start_at, end_at)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# ── 3. RLS policy ─────────────────────────────────────────────────────
|
||||
conn.execute(sa.text("ALTER TABLE maintenance_windows ENABLE ROW LEVEL SECURITY"))
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -67,10 +72,12 @@ def upgrade() -> None:
|
||||
END IF;
|
||||
END
|
||||
$$
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# ── 4. Grant permissions to app_user ───────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN
|
||||
@@ -78,7 +85,8 @@ def upgrade() -> None:
|
||||
END IF;
|
||||
END
|
||||
$$
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -28,34 +28,81 @@ def upgrade() -> None:
|
||||
# ── vpn_config: one row per tenant ──
|
||||
op.create_table(
|
||||
"vpn_config",
|
||||
sa.Column("id", UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), primary_key=True),
|
||||
sa.Column("tenant_id", UUID(as_uuid=True), sa.ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, unique=True),
|
||||
sa.Column(
|
||||
"id", UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), primary_key=True
|
||||
),
|
||||
sa.Column(
|
||||
"tenant_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
),
|
||||
sa.Column("server_private_key", sa.LargeBinary(), nullable=False), # AES-256-GCM encrypted
|
||||
sa.Column("server_public_key", sa.String(64), nullable=False),
|
||||
sa.Column("subnet", sa.String(32), nullable=False, server_default="10.10.0.0/24"),
|
||||
sa.Column("server_port", sa.Integer(), nullable=False, server_default="51820"),
|
||||
sa.Column("server_address", sa.String(32), nullable=False, server_default="10.10.0.1/24"),
|
||||
sa.Column("endpoint", sa.String(255), nullable=True), # public hostname:port for devices to connect to
|
||||
sa.Column(
|
||||
"endpoint", sa.String(255), nullable=True
|
||||
), # public hostname:port for devices to connect to
|
||||
sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
# ── vpn_peers: one per device VPN connection ──
|
||||
op.create_table(
|
||||
"vpn_peers",
|
||||
sa.Column("id", UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), primary_key=True),
|
||||
sa.Column("tenant_id", UUID(as_uuid=True), sa.ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("device_id", UUID(as_uuid=True), sa.ForeignKey("devices.id", ondelete="CASCADE"), nullable=False, unique=True),
|
||||
sa.Column(
|
||||
"id", UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), primary_key=True
|
||||
),
|
||||
sa.Column(
|
||||
"tenant_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("tenants.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"device_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("devices.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
),
|
||||
sa.Column("peer_private_key", sa.LargeBinary(), nullable=False), # AES-256-GCM encrypted
|
||||
sa.Column("peer_public_key", sa.String(64), nullable=False),
|
||||
sa.Column("preshared_key", sa.LargeBinary(), nullable=True), # AES-256-GCM encrypted, optional
|
||||
sa.Column(
|
||||
"preshared_key", sa.LargeBinary(), nullable=True
|
||||
), # AES-256-GCM encrypted, optional
|
||||
sa.Column("assigned_ip", sa.String(32), nullable=False), # e.g. 10.10.0.2/24
|
||||
sa.Column("additional_allowed_ips", sa.String(512), nullable=True), # comma-separated subnets for site-to-site
|
||||
sa.Column(
|
||||
"additional_allowed_ips", sa.String(512), nullable=True
|
||||
), # comma-separated subnets for site-to-site
|
||||
sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("last_handshake", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
# Indexes
|
||||
|
||||
@@ -22,7 +22,8 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# 1. Basic Router — comprehensive starter for a typical SOHO/branch router
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -75,10 +76,12 @@ add chain=forward action=drop comment="Drop everything else"
|
||||
SELECT 1 FROM config_templates ct
|
||||
WHERE ct.tenant_id = t.id AND ct.name = 'Basic Router'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 2. Re-seed Basic Firewall (for tenants missing it)
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -100,10 +103,12 @@ add chain=forward action=drop',
|
||||
SELECT 1 FROM config_templates ct
|
||||
WHERE ct.tenant_id = t.id AND ct.name = 'Basic Firewall'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 3. Re-seed DHCP Server Setup
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -119,10 +124,12 @@ add chain=forward action=drop',
|
||||
SELECT 1 FROM config_templates ct
|
||||
WHERE ct.tenant_id = t.id AND ct.name = 'DHCP Server Setup'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 4. Re-seed Wireless AP Config
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -137,10 +144,12 @@ add chain=forward action=drop',
|
||||
SELECT 1 FROM config_templates ct
|
||||
WHERE ct.tenant_id = t.id AND ct.name = 'Wireless AP Config'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# 5. Re-seed Initial Device Setup
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO config_templates (id, tenant_id, name, description, content, variables)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
@@ -159,11 +168,10 @@ add chain=forward action=drop',
|
||||
SELECT 1 FROM config_templates ct
|
||||
WHERE ct.tenant_id = t.id AND ct.name = 'Initial Device Setup'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
conn.execute(sa.text(
|
||||
"DELETE FROM config_templates WHERE name = 'Basic Router'"
|
||||
))
|
||||
conn.execute(sa.text("DELETE FROM config_templates WHERE name = 'Basic Router'"))
|
||||
|
||||
@@ -138,62 +138,44 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# certificate_authorities RLS
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE certificate_authorities ENABLE ROW LEVEL SECURITY"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON certificate_authorities TO app_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE POLICY tenant_isolation ON certificate_authorities FOR ALL "
|
||||
"USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid) "
|
||||
"WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT ON certificate_authorities TO poller_user"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE certificate_authorities ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(
|
||||
sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON certificate_authorities TO app_user")
|
||||
)
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE POLICY tenant_isolation ON certificate_authorities FOR ALL "
|
||||
"USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid) "
|
||||
"WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)"
|
||||
)
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT ON certificate_authorities TO poller_user"))
|
||||
|
||||
# device_certificates RLS
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE device_certificates ENABLE ROW LEVEL SECURITY"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE, DELETE ON device_certificates TO app_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE POLICY tenant_isolation ON device_certificates FOR ALL "
|
||||
"USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid) "
|
||||
"WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT ON device_certificates TO poller_user"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE device_certificates ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE, DELETE ON device_certificates TO app_user"))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE POLICY tenant_isolation ON device_certificates FOR ALL "
|
||||
"USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid) "
|
||||
"WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)"
|
||||
)
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT ON device_certificates TO poller_user"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Drop RLS policies
|
||||
conn.execute(sa.text(
|
||||
"DROP POLICY IF EXISTS tenant_isolation ON device_certificates"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"DROP POLICY IF EXISTS tenant_isolation ON certificate_authorities"
|
||||
))
|
||||
conn.execute(sa.text("DROP POLICY IF EXISTS tenant_isolation ON device_certificates"))
|
||||
conn.execute(sa.text("DROP POLICY IF EXISTS tenant_isolation ON certificate_authorities"))
|
||||
|
||||
# Revoke grants
|
||||
conn.execute(sa.text(
|
||||
"REVOKE ALL ON device_certificates FROM app_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"REVOKE ALL ON device_certificates FROM poller_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"REVOKE ALL ON certificate_authorities FROM app_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"REVOKE ALL ON certificate_authorities FROM poller_user"
|
||||
))
|
||||
conn.execute(sa.text("REVOKE ALL ON device_certificates FROM app_user"))
|
||||
conn.execute(sa.text("REVOKE ALL ON device_certificates FROM poller_user"))
|
||||
conn.execute(sa.text("REVOKE ALL ON certificate_authorities FROM app_user"))
|
||||
conn.execute(sa.text("REVOKE ALL ON certificate_authorities FROM poller_user"))
|
||||
|
||||
# Drop tls_mode column from devices
|
||||
op.drop_column("devices", "tls_mode")
|
||||
|
||||
@@ -35,9 +35,7 @@ def upgrade() -> None:
|
||||
|
||||
for table in HYPERTABLES:
|
||||
# Drop chunks older than 90 days
|
||||
conn.execute(sa.text(
|
||||
f"SELECT add_retention_policy('{table}', INTERVAL '90 days')"
|
||||
))
|
||||
conn.execute(sa.text(f"SELECT add_retention_policy('{table}', INTERVAL '90 days')"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
@@ -45,6 +43,4 @@ def downgrade() -> None:
|
||||
|
||||
for table in HYPERTABLES:
|
||||
# Remove retention policy
|
||||
conn.execute(sa.text(
|
||||
f"SELECT remove_retention_policy('{table}', if_exists => true)"
|
||||
))
|
||||
conn.execute(sa.text(f"SELECT remove_retention_policy('{table}', if_exists => true)"))
|
||||
|
||||
@@ -147,46 +147,36 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# user_key_sets RLS
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE user_key_sets ENABLE ROW LEVEL SECURITY"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE POLICY user_key_sets_tenant_isolation ON user_key_sets "
|
||||
"USING (tenant_id::text = current_setting('app.current_tenant', true) "
|
||||
"OR current_setting('app.current_tenant', true) = 'super_admin')"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT SELECT, INSERT, UPDATE ON user_key_sets TO app_user"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE user_key_sets ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE POLICY user_key_sets_tenant_isolation ON user_key_sets "
|
||||
"USING (tenant_id::text = current_setting('app.current_tenant', true) "
|
||||
"OR current_setting('app.current_tenant', true) = 'super_admin')"
|
||||
)
|
||||
)
|
||||
conn.execute(sa.text("GRANT SELECT, INSERT, UPDATE ON user_key_sets TO app_user"))
|
||||
|
||||
# key_access_log RLS (append-only: INSERT+SELECT only, no UPDATE/DELETE)
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE key_access_log ENABLE ROW LEVEL SECURITY"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"CREATE POLICY key_access_log_tenant_isolation ON key_access_log "
|
||||
"USING (tenant_id::text = current_setting('app.current_tenant', true) "
|
||||
"OR current_setting('app.current_tenant', true) = 'super_admin')"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"GRANT INSERT, SELECT ON key_access_log TO app_user"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE key_access_log ENABLE ROW LEVEL SECURITY"))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"CREATE POLICY key_access_log_tenant_isolation ON key_access_log "
|
||||
"USING (tenant_id::text = current_setting('app.current_tenant', true) "
|
||||
"OR current_setting('app.current_tenant', true) = 'super_admin')"
|
||||
)
|
||||
)
|
||||
conn.execute(sa.text("GRANT INSERT, SELECT ON key_access_log TO app_user"))
|
||||
# poller_user needs INSERT to log key access events when decrypting credentials
|
||||
conn.execute(sa.text(
|
||||
"GRANT INSERT, SELECT ON key_access_log TO poller_user"
|
||||
))
|
||||
conn.execute(sa.text("GRANT INSERT, SELECT ON key_access_log TO poller_user"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Drop RLS policies
|
||||
conn.execute(sa.text(
|
||||
"DROP POLICY IF EXISTS key_access_log_tenant_isolation ON key_access_log"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"DROP POLICY IF EXISTS user_key_sets_tenant_isolation ON user_key_sets"
|
||||
))
|
||||
conn.execute(sa.text("DROP POLICY IF EXISTS key_access_log_tenant_isolation ON key_access_log"))
|
||||
conn.execute(sa.text("DROP POLICY IF EXISTS user_key_sets_tenant_isolation ON user_key_sets"))
|
||||
|
||||
# Revoke grants
|
||||
conn.execute(sa.text("REVOKE ALL ON key_access_log FROM app_user"))
|
||||
|
||||
@@ -77,9 +77,7 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_key_access_log_device_id", "key_access_log", type_="foreignkey"
|
||||
)
|
||||
op.drop_constraint("fk_key_access_log_device_id", "key_access_log", type_="foreignkey")
|
||||
op.drop_column("key_access_log", "correlation_id")
|
||||
op.drop_column("key_access_log", "justification")
|
||||
op.drop_column("key_access_log", "device_id")
|
||||
|
||||
@@ -33,8 +33,7 @@ def upgrade() -> None:
|
||||
|
||||
# Flag all bcrypt-only users for upgrade (auth_version=1 and no SRP verifier)
|
||||
op.execute(
|
||||
"UPDATE users SET must_upgrade_auth = true "
|
||||
"WHERE auth_version = 1 AND srp_verifier IS NULL"
|
||||
"UPDATE users SET must_upgrade_auth = true WHERE auth_version = 1 AND srp_verifier IS NULL"
|
||||
)
|
||||
|
||||
# Make hashed_password nullable (SRP users don't need it)
|
||||
@@ -44,8 +43,7 @@ def upgrade() -> None:
|
||||
def downgrade() -> None:
|
||||
# Restore NOT NULL (set a dummy value for any NULLs first)
|
||||
op.execute(
|
||||
"UPDATE users SET hashed_password = '$2b$12$placeholder' "
|
||||
"WHERE hashed_password IS NULL"
|
||||
"UPDATE users SET hashed_password = '$2b$12$placeholder' WHERE hashed_password IS NULL"
|
||||
)
|
||||
op.alter_column("users", "hashed_password", nullable=False)
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ Existing 'insecure' devices become 'auto' since the old behavior was
|
||||
an implicit auto-fallback. portal_ca devices keep their mode.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "020"
|
||||
|
||||
@@ -25,7 +25,8 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in _TABLES:
|
||||
conn.execute(sa.text(f"DROP POLICY IF EXISTS tenant_isolation ON {table}"))
|
||||
conn.execute(sa.text(f"""
|
||||
conn.execute(
|
||||
sa.text(f"""
|
||||
CREATE POLICY tenant_isolation ON {table}
|
||||
USING (
|
||||
tenant_id::text = current_setting('app.current_tenant', true)
|
||||
@@ -35,15 +36,18 @@ def upgrade() -> None:
|
||||
tenant_id::text = current_setting('app.current_tenant', true)
|
||||
OR current_setting('app.current_tenant', true) = 'super_admin'
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in _TABLES:
|
||||
conn.execute(sa.text(f"DROP POLICY IF EXISTS tenant_isolation ON {table}"))
|
||||
conn.execute(sa.text(f"""
|
||||
conn.execute(
|
||||
sa.text(f"""
|
||||
CREATE POLICY tenant_isolation ON {table}
|
||||
USING (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
WITH CHECK (tenant_id::text = current_setting('app.current_tenant', true))
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
@@ -19,7 +19,8 @@ def upgrade() -> None:
|
||||
op.add_column("tenants", sa.Column("contact_email", sa.String(255), nullable=True))
|
||||
|
||||
# 2. Seed device_offline default alert rule for all existing tenants
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO alert_rules (id, tenant_id, name, metric, operator, threshold, duration_polls, severity, enabled, is_default)
|
||||
SELECT gen_random_uuid(), t.id, 'Device Offline', 'device_offline', 'eq', 1, 1, 'critical', TRUE, TRUE
|
||||
FROM tenants t
|
||||
@@ -28,14 +29,17 @@ def upgrade() -> None:
|
||||
SELECT 1 FROM alert_rules ar
|
||||
WHERE ar.tenant_id = t.id AND ar.metric = 'device_offline' AND ar.is_default = TRUE
|
||||
)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(sa.text("""
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
DELETE FROM alert_rules WHERE metric = 'device_offline' AND is_default = TRUE
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
op.drop_column("tenants", "contact_email")
|
||||
|
||||
@@ -11,9 +11,7 @@ down_revision = "024"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_key_access_log_device_id", "key_access_log", type_="foreignkey"
|
||||
)
|
||||
op.drop_constraint("fk_key_access_log_device_id", "key_access_log", type_="foreignkey")
|
||||
op.create_foreign_key(
|
||||
"fk_key_access_log_device_id",
|
||||
"key_access_log",
|
||||
@@ -25,9 +23,7 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_key_access_log_device_id", "key_access_log", type_="foreignkey"
|
||||
)
|
||||
op.drop_constraint("fk_key_access_log_device_id", "key_access_log", type_="foreignkey")
|
||||
op.create_foreign_key(
|
||||
"fk_key_access_log_device_id",
|
||||
"key_access_log",
|
||||
|
||||
@@ -25,7 +25,8 @@ def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# ── router_config_snapshots ──────────────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
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,
|
||||
@@ -35,30 +36,38 @@ def upgrade() -> None:
|
||||
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("""
|
||||
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)"
|
||||
))
|
||||
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("""
|
||||
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,
|
||||
@@ -70,27 +79,33 @@ def upgrade() -> None:
|
||||
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("""
|
||||
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)"
|
||||
))
|
||||
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("""
|
||||
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,
|
||||
@@ -101,24 +116,25 @@ def upgrade() -> None:
|
||||
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("""
|
||||
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)"
|
||||
))
|
||||
conn.execute(sa.text("CREATE INDEX idx_rcc_diff_id ON router_config_changes (diff_id)"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -25,40 +25,28 @@ import sqlalchemy as sa
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN ssh_port INTEGER DEFAULT 22"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN ssh_host_key_fingerprint TEXT"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN ssh_host_key_first_seen TIMESTAMPTZ"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices ADD COLUMN ssh_host_key_last_verified TIMESTAMPTZ"
|
||||
))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN ssh_port INTEGER DEFAULT 22"))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN ssh_host_key_fingerprint TEXT"))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN ssh_host_key_first_seen TIMESTAMPTZ"))
|
||||
conn.execute(sa.text("ALTER TABLE devices ADD COLUMN ssh_host_key_last_verified TIMESTAMPTZ"))
|
||||
|
||||
# Grant poller_user UPDATE on SSH columns for TOFU host key persistence
|
||||
conn.execute(sa.text(
|
||||
"GRANT UPDATE (ssh_host_key_fingerprint, ssh_host_key_first_seen, ssh_host_key_last_verified) ON devices TO poller_user"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"GRANT UPDATE (ssh_host_key_fingerprint, ssh_host_key_first_seen, ssh_host_key_last_verified) ON devices TO poller_user"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
conn.execute(sa.text(
|
||||
"REVOKE UPDATE (ssh_host_key_fingerprint, ssh_host_key_first_seen, ssh_host_key_last_verified) ON devices FROM poller_user"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices DROP COLUMN ssh_host_key_last_verified"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices DROP COLUMN ssh_host_key_first_seen"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices DROP COLUMN ssh_host_key_fingerprint"
|
||||
))
|
||||
conn.execute(sa.text(
|
||||
"ALTER TABLE devices DROP COLUMN ssh_port"
|
||||
))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"REVOKE UPDATE (ssh_host_key_fingerprint, ssh_host_key_first_seen, ssh_host_key_last_verified) ON devices FROM poller_user"
|
||||
)
|
||||
)
|
||||
conn.execute(sa.text("ALTER TABLE devices DROP COLUMN ssh_host_key_last_verified"))
|
||||
conn.execute(sa.text("ALTER TABLE devices DROP COLUMN ssh_host_key_first_seen"))
|
||||
conn.execute(sa.text("ALTER TABLE devices DROP COLUMN ssh_host_key_fingerprint"))
|
||||
conn.execute(sa.text("ALTER TABLE devices DROP COLUMN ssh_port"))
|
||||
|
||||
@@ -16,7 +16,12 @@ import base64
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
NoEncryption,
|
||||
PrivateFormat,
|
||||
PublicFormat,
|
||||
)
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
|
||||
@@ -120,5 +125,9 @@ def downgrade() -> None:
|
||||
op.alter_column("vpn_config", "subnet", server_default="10.10.0.0/24")
|
||||
op.alter_column("vpn_config", "server_address", server_default="10.10.0.1/24")
|
||||
conn = op.get_bind()
|
||||
conn.execute(sa.text("DELETE FROM system_settings WHERE key IN ('vpn_server_public_key', 'vpn_server_private_key')"))
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"DELETE FROM system_settings WHERE key IN ('vpn_server_public_key', 'vpn_server_private_key')"
|
||||
)
|
||||
)
|
||||
# NOTE: downgrade does not remap peer IPs back. Manual cleanup may be needed.
|
||||
|
||||
Reference in New Issue
Block a user