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:
Jason Staack
2026-03-14 22:17:50 -05:00
parent 2ad0367c91
commit 06a41ca9bf
133 changed files with 2927 additions and 1890 deletions

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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'"))

View File

@@ -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")

View File

@@ -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)"))

View File

@@ -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"))

View File

@@ -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")

View File

@@ -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)

View File

@@ -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"

View File

@@ -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))
"""))
""")
)

View File

@@ -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")

View File

@@ -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",

View File

@@ -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:

View File

@@ -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"))

View File

@@ -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.