feat: The Other Dude v9.0.1 — full-featured email system

ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-08 17:46:37 -05:00
commit b840047e19
511 changed files with 106948 additions and 0 deletions

View File

@@ -0,0 +1,376 @@
"""Initial schema with RLS policies for multi-tenant isolation.
Revision ID: 001
Revises: None
Create Date: 2026-02-24
This migration creates:
1. All database tables (tenants, users, devices, device_groups, device_tags,
device_group_memberships, device_tag_assignments)
2. Composite unique indexes for tenant-scoped uniqueness
3. Row Level Security (RLS) on all tenant-scoped tables
4. RLS policies using app.current_tenant PostgreSQL setting
5. The app_user role with appropriate grants (cannot bypass RLS)
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# =========================================================================
# TENANTS TABLE
# =========================================================================
op.create_table(
"tenants",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("description", sa.Text, 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.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_index("ix_tenants_name", "tenants", ["name"], unique=True)
# =========================================================================
# USERS TABLE
# =========================================================================
op.create_table(
"users",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("email", sa.String(255), nullable=False),
sa.Column("hashed_password", sa.String(255), nullable=False),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("role", sa.String(50), nullable=False, server_default="viewer"),
sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
sa.Column("last_login", 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.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"),
)
op.create_index("ix_users_email", "users", ["email"], unique=True)
op.create_index("ix_users_tenant_id", "users", ["tenant_id"])
# =========================================================================
# DEVICES TABLE
# =========================================================================
op.create_table(
"devices",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("hostname", sa.String(255), nullable=False),
sa.Column("ip_address", sa.String(45), nullable=False),
sa.Column("api_port", sa.Integer, nullable=False, server_default="8728"),
sa.Column("api_ssl_port", sa.Integer, nullable=False, server_default="8729"),
sa.Column("model", sa.String(255), nullable=True),
sa.Column("serial_number", sa.String(255), nullable=True),
sa.Column("firmware_version", sa.String(100), nullable=True),
sa.Column("routeros_version", sa.String(100), nullable=True),
sa.Column("uptime_seconds", sa.Integer, nullable=True),
sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True),
sa.Column("encrypted_credentials", sa.LargeBinary, nullable=True),
sa.Column("status", sa.String(20), nullable=False, server_default="unknown"),
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.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"),
sa.UniqueConstraint("tenant_id", "hostname", name="uq_devices_tenant_hostname"),
)
op.create_index("ix_devices_tenant_id", "devices", ["tenant_id"])
# =========================================================================
# DEVICE GROUPS TABLE
# =========================================================================
op.create_table(
"device_groups",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"),
sa.UniqueConstraint("tenant_id", "name", name="uq_device_groups_tenant_name"),
)
op.create_index("ix_device_groups_tenant_id", "device_groups", ["tenant_id"])
# =========================================================================
# DEVICE TAGS TABLE
# =========================================================================
op.create_table(
"device_tags",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("color", sa.String(7), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"),
sa.UniqueConstraint("tenant_id", "name", name="uq_device_tags_tenant_name"),
)
op.create_index("ix_device_tags_tenant_id", "device_tags", ["tenant_id"])
# =========================================================================
# DEVICE GROUP MEMBERSHIPS TABLE
# =========================================================================
op.create_table(
"device_group_memberships",
sa.Column("device_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("group_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.PrimaryKeyConstraint("device_id", "group_id"),
sa.ForeignKeyConstraint(["device_id"], ["devices.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["group_id"], ["device_groups.id"], ondelete="CASCADE"),
)
# =========================================================================
# DEVICE TAG ASSIGNMENTS TABLE
# =========================================================================
op.create_table(
"device_tag_assignments",
sa.Column("device_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("tag_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.PrimaryKeyConstraint("device_id", "tag_id"),
sa.ForeignKeyConstraint(["device_id"], ["devices.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["tag_id"], ["device_tags.id"], ondelete="CASCADE"),
)
# =========================================================================
# ROW LEVEL SECURITY (RLS)
# =========================================================================
# RLS is the core tenant isolation mechanism. The app_user role CANNOT
# bypass RLS (only superusers can). All queries through app_user will
# be filtered by the current_setting('app.current_tenant') value which
# is set per-request by the tenant_context middleware.
conn = op.get_bind()
# --- TENANTS RLS ---
# 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("""
CREATE POLICY tenant_isolation ON tenants
USING (
id::text = current_setting('app.current_tenant', true)
OR current_setting('app.current_tenant', true) = 'super_admin'
)
WITH CHECK (
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("""
CREATE POLICY tenant_isolation ON users
USING (
tenant_id::text = current_setting('app.current_tenant', true)
OR current_setting('app.current_tenant', true) = 'super_admin'
)
WITH CHECK (
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("""
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("""
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("""
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("""
CREATE POLICY tenant_isolation ON device_group_memberships
USING (
EXISTS (
SELECT 1 FROM devices d
WHERE d.id = device_id
AND d.tenant_id::text = current_setting('app.current_tenant', true)
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM devices d
WHERE d.id = device_id
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("""
CREATE POLICY tenant_isolation ON device_tag_assignments
USING (
EXISTS (
SELECT 1 FROM devices d
WHERE d.id = device_id
AND d.tenant_id::text = current_setting('app.current_tenant', true)
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM devices d
WHERE d.id = device_id
AND d.tenant_id::text = current_setting('app.current_tenant', true)
)
)
"""))
# =========================================================================
# GRANT PERMISSIONS TO app_user (RLS-enforcing application role)
# =========================================================================
# app_user is a non-superuser role — it CANNOT bypass RLS policies.
# All API queries use this role to ensure tenant isolation.
tables = [
"tenants",
"users",
"devices",
"device_groups",
"device_tags",
"device_group_memberships",
"device_tag_assignments",
]
for table in tables:
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"))
# Allow app_user to set the tenant context variable
conn.execute(sa.text("GRANT SET ON PARAMETER app.current_tenant TO app_user"))
def downgrade() -> None:
conn = op.get_bind()
# Revoke grants
tables = [
"tenants",
"users",
"devices",
"device_groups",
"device_tags",
"device_group_memberships",
"device_tag_assignments",
]
for table in tables:
try:
conn.execute(sa.text(f"REVOKE ALL ON {table} FROM app_user"))
except Exception:
pass
# Drop tables (in reverse dependency order)
op.drop_table("device_tag_assignments")
op.drop_table("device_group_memberships")
op.drop_table("device_tags")
op.drop_table("device_groups")
op.drop_table("devices")
op.drop_table("users")
op.drop_table("tenants")