Add multi-IP binding modes and deployment guide

This commit is contained in:
2026-03-04 15:30:13 +08:00
parent 4348ee799b
commit eed1acd454
12 changed files with 509 additions and 217 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
@@ -40,6 +41,31 @@ def get_session_factory() -> async_sessionmaker[AsyncSession]:
return _session_factory
async def ensure_schema_compatibility() -> None:
engine = get_engine()
statements = [
"DROP INDEX IF EXISTS idx_token_bindings_ip",
"ALTER TABLE token_bindings ALTER COLUMN bound_ip TYPE TEXT USING bound_ip::text",
"ALTER TABLE intercept_logs ALTER COLUMN bound_ip TYPE TEXT USING bound_ip::text",
"ALTER TABLE token_bindings ADD COLUMN IF NOT EXISTS binding_mode VARCHAR(16) DEFAULT 'single'",
"ALTER TABLE token_bindings ADD COLUMN IF NOT EXISTS allowed_ips JSONB DEFAULT '[]'::jsonb",
"UPDATE token_bindings SET binding_mode = 'single' WHERE binding_mode IS NULL OR binding_mode = ''",
"""
UPDATE token_bindings
SET allowed_ips = jsonb_build_array(bound_ip)
WHERE allowed_ips IS NULL OR allowed_ips = '[]'::jsonb
""",
"ALTER TABLE token_bindings ALTER COLUMN binding_mode SET NOT NULL",
"ALTER TABLE token_bindings ALTER COLUMN allowed_ips SET NOT NULL",
"ALTER TABLE token_bindings ALTER COLUMN binding_mode SET DEFAULT 'single'",
"ALTER TABLE token_bindings ALTER COLUMN allowed_ips SET DEFAULT '[]'::jsonb",
"CREATE INDEX IF NOT EXISTS idx_token_bindings_ip ON token_bindings(bound_ip)",
]
async with engine.begin() as connection:
for statement in statements:
await connection.execute(text(statement))
async def close_db() -> None:
global _engine, _session_factory
if _engine is not None:

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Index, String, func, text
from sqlalchemy.dialects.postgresql import CIDR, INET
from sqlalchemy import Boolean, DateTime, Index, String, Text, func, text
from sqlalchemy.dialects.postgresql import INET
from sqlalchemy.orm import Mapped, mapped_column
from app.models.db import Base
@@ -19,7 +19,7 @@ class InterceptLog(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
token_hash: Mapped[str] = mapped_column(String(64), nullable=False)
token_display: Mapped[str] = mapped_column(String(20), nullable=False)
bound_ip: Mapped[str] = mapped_column(CIDR, nullable=False)
bound_ip: Mapped[str] = mapped_column(Text, nullable=False)
attempt_ip: Mapped[str] = mapped_column(INET, nullable=False)
alerted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=text("FALSE"))
intercepted_at: Mapped[datetime] = mapped_column(

View File

@@ -2,27 +2,42 @@ from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Index, SmallInteger, String, func, text
from sqlalchemy.dialects.postgresql import CIDR
from sqlalchemy import DateTime, Index, SmallInteger, String, Text, func, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.db import Base
STATUS_ACTIVE = 1
STATUS_BANNED = 2
BINDING_MODE_SINGLE = "single"
BINDING_MODE_MULTIPLE = "multiple"
BINDING_MODE_ALL = "all"
class TokenBinding(Base):
__tablename__ = "token_bindings"
__table_args__ = (
Index("idx_token_bindings_hash", "token_hash"),
Index("idx_token_bindings_ip", "bound_ip", postgresql_using="gist", postgresql_ops={"bound_ip": "inet_ops"}),
Index("idx_token_bindings_ip", "bound_ip"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
token_display: Mapped[str] = mapped_column(String(20), nullable=False)
bound_ip: Mapped[str] = mapped_column(CIDR, nullable=False)
bound_ip: Mapped[str] = mapped_column(Text, nullable=False)
binding_mode: Mapped[str] = mapped_column(
String(16),
nullable=False,
default=BINDING_MODE_SINGLE,
server_default=text("'single'"),
)
allowed_ips: Mapped[list[str]] = mapped_column(
JSONB,
nullable=False,
default=list,
server_default=text("'[]'::jsonb"),
)
status: Mapped[int] = mapped_column(
SmallInteger,
nullable=False,