8530f0e0d5
P0-1: PHP配置注入防护 — install.py添加_escape_php_string()转义单引号和反斜杠 P0-2: WebSocket JWT校验 — _verify_ws_token()要求exp+sub字段 P1-1: 删除SHA256密码fallback — auth_service.py和auth.py直接import bcrypt P1-3: LIKE通配符转义 — search.py添加_escape_like()并对所有ilike()加escape参数 P1-2: 安全响应头中间件 — main.py添加SecurityHeadersMiddleware注入4个安全头 P0-3: Refresh Token重用检测 — Admin模型添加token_version字段,token格式改为token:admin_id:version Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
363 lines
16 KiB
Python
363 lines
16 KiB
Python
"""Nexus — Domain Models
|
|
SQLAlchemy ORM models for all database tables.
|
|
"""
|
|
|
|
import datetime
|
|
from datetime import timezone
|
|
import uuid
|
|
import base64
|
|
import hashlib
|
|
from sqlalchemy import (
|
|
create_engine, Column, Integer, String, Boolean,
|
|
DateTime, Text, ForeignKey, Enum, Index, JSON
|
|
)
|
|
from sqlalchemy.orm import declarative_base, relationship
|
|
|
|
|
|
def _utcnow():
|
|
"""Callable default for DateTime columns — evaluated per-row, not at module import"""
|
|
return datetime.datetime.now(timezone.utc)
|
|
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Platform & Node (资产组织层)
|
|
# ──────────────────────────────────────────────
|
|
|
|
class Platform(Base):
|
|
"""Asset type template — defines what kind of asset this is"""
|
|
__tablename__ = "platforms"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), unique=True, nullable=False, comment="平台名称: Linux服务器/Windows/MySQL")
|
|
category = Column(String(50), nullable=False, comment="分类: host/database/device")
|
|
type = Column(String(50), nullable=False, comment="类型: linux/windows/mysql/switch")
|
|
default_protocols = Column(JSON, nullable=True, comment="默认协议: [{name:'ssh',port:22,primary:true}]")
|
|
charset = Column(String(20), default="utf-8", comment="默认字符集")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
assets = relationship("Server", back_populates="platform")
|
|
|
|
|
|
class Node(Base):
|
|
"""Tree-structured asset grouping"""
|
|
__tablename__ = "nodes"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False, comment="节点名称")
|
|
parent_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="父节点")
|
|
sort_order = Column(Integer, default=0, comment="排序权重")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
parent = relationship("Node", remote_side=[id], back_populates="children")
|
|
children = relationship("Node", back_populates="parent")
|
|
assets = relationship("Server", back_populates="node")
|
|
|
|
__table_args__ = (
|
|
Index('idx_nodes_parent_id', 'parent_id'),
|
|
)
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Server (核心资产表 — 保持原名,扩展字段)
|
|
# ──────────────────────────────────────────────
|
|
|
|
class Server(Base):
|
|
"""Sub-server configuration / Asset"""
|
|
__tablename__ = "servers"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), unique=True, nullable=False, comment="服务器名称")
|
|
domain = Column(String(255), nullable=False, comment="域名或IP")
|
|
port = Column(Integer, default=22, comment="SSH端口")
|
|
username = Column(String(100), default="root", comment="SSH用户名")
|
|
auth_method = Column(String(20), default="key", comment="认证方式: key/password")
|
|
password = Column(String(255), nullable=True, comment="SSH密码(加密存储)")
|
|
ssh_key_path = Column(String(500), nullable=True, comment="SSH私钥路径")
|
|
ssh_key_private = Column(String(500), nullable=True, comment="加密后的私钥内容")
|
|
ssh_key_public = Column(String(500), nullable=True, comment="公钥内容")
|
|
ssh_key_configured = Column(Boolean, default=False, comment="是否已配置SSH Key")
|
|
agent_port = Column(Integer, default=8601, comment="Agent API端口")
|
|
agent_api_key = Column(String(255), nullable=True, comment="Agent API密钥")
|
|
description = Column(Text, nullable=True, comment="备注说明")
|
|
target_path = Column(String(500), nullable=True, comment="推送目标路径")
|
|
category = Column(String(100), nullable=True, comment="服务器分类(旧字段,保留兼容)")
|
|
|
|
# ── 新增:资产组织字段 ──
|
|
platform_id = Column(Integer, ForeignKey("platforms.id", ondelete="SET NULL"), nullable=True, comment="平台类型ID")
|
|
node_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="所属节点ID")
|
|
protocols = Column(JSON, nullable=True, comment="实例级协议覆盖: [{name:'ssh',port:2222}]")
|
|
extra_attrs = Column(JSON, nullable=True, comment="扩展属性: {os:'ubuntu',arch:'x86_64',cpu:8,mem:'32G'}")
|
|
|
|
# Status (from Agent heartbeat)
|
|
is_online = Column(Boolean, default=False, comment="是否在线")
|
|
last_heartbeat = Column(DateTime, nullable=True, comment="最后心跳时间")
|
|
system_info = Column(Text, nullable=True, comment="系统信息JSON(Agent上报)")
|
|
agent_version = Column(String(20), nullable=True, comment="Agent版本")
|
|
|
|
# ── 新增:连接状态 ──
|
|
connectivity = Column(String(20), default="unknown", comment="连接状态: ok/err/unknown")
|
|
last_checked_at = Column(DateTime, nullable=True, comment="上次连接测试时间")
|
|
|
|
# Metadata
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
|
|
|
# Relationships
|
|
sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan")
|
|
platform = relationship("Platform", back_populates="assets")
|
|
node = relationship("Node", back_populates="assets")
|
|
|
|
__table_args__ = (
|
|
Index('idx_servers_is_online', 'is_online'),
|
|
Index('idx_servers_category', 'category'),
|
|
Index('idx_servers_platform_id', 'platform_id'),
|
|
Index('idx_servers_node_id', 'node_id'),
|
|
)
|
|
|
|
|
|
class SyncLog(Base):
|
|
"""Push operation log"""
|
|
__tablename__ = "sync_logs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
|
|
source_path = Column(String(500), nullable=False, comment="源目录")
|
|
target_path = Column(String(500), nullable=False, comment="目标目录")
|
|
trigger_type = Column(String(20), comment="触发类型: manual/schedule/batch")
|
|
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
|
status = Column(String(20), comment="pending/running/success/failed")
|
|
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
|
|
files_total = Column(Integer, default=0)
|
|
files_transferred = Column(Integer, default=0)
|
|
files_skipped = Column(Integer, default=0)
|
|
bytes_transferred = Column(Integer, default=0)
|
|
duration_seconds = Column(Integer, default=0)
|
|
diff_summary = Column(Text, nullable=True, comment="rsync输出摘要")
|
|
error_message = Column(Text, nullable=True)
|
|
started_at = Column(DateTime, default=_utcnow)
|
|
finished_at = Column(DateTime, nullable=True)
|
|
|
|
server = relationship("Server", back_populates="sync_logs")
|
|
|
|
__table_args__ = (
|
|
Index('idx_sync_logs_srv_start', 'server_id', 'started_at'),
|
|
)
|
|
|
|
|
|
class Admin(Base):
|
|
"""Admin user for web login"""
|
|
__tablename__ = "admins"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
username = Column(String(100), unique=True, nullable=False)
|
|
password_hash = Column(String(255), nullable=False)
|
|
email = Column(String(255), nullable=True)
|
|
totp_secret = Column(String(64), nullable=True)
|
|
totp_enabled = Column(Boolean, default=False)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
|
last_login = Column(DateTime, nullable=True)
|
|
|
|
# ── 新增:JWT 支持 ──
|
|
jwt_refresh_token = Column(String(500), nullable=True, comment="JWT刷新令牌")
|
|
jwt_token_expires = Column(DateTime, nullable=True, comment="令牌过期时间")
|
|
token_version = Column(Integer, default=0, comment="令牌版本(重用时递增使旧token失效)")
|
|
|
|
|
|
class LoginAttempt(Base):
|
|
"""Login attempt log for brute-force protection"""
|
|
__tablename__ = "login_attempts"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
username = Column(String(100), nullable=False)
|
|
ip_address = Column(String(45), nullable=False)
|
|
attempted_at = Column(DateTime, default=_utcnow)
|
|
success = Column(Boolean, default=False)
|
|
|
|
__table_args__ = (
|
|
Index('idx_login_attempts_user_time', 'username', 'attempted_at'),
|
|
)
|
|
|
|
|
|
class Setting(Base):
|
|
"""Key-value system settings (including brand config)"""
|
|
__tablename__ = "settings"
|
|
|
|
key = Column(String(100), primary_key=True)
|
|
value = Column(Text, nullable=True)
|
|
updated_at = Column(DateTime, default=_utcnow)
|
|
|
|
|
|
class PasswordPreset(Base):
|
|
"""SSH password preset — encrypted storage"""
|
|
__tablename__ = "password_presets"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False, comment="预设名称")
|
|
encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
|
|
class PushSchedule(Base):
|
|
"""Scheduled push — cron expression trigger"""
|
|
__tablename__ = "push_schedules"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False, comment="调度名称")
|
|
source_path = Column(String(500), nullable=False)
|
|
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
|
|
cron_expr = Column(String(100), nullable=False, comment="cron表达式: 分 时 日 月 周")
|
|
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
|
|
enabled = Column(Boolean, default=True)
|
|
last_run_at = Column(DateTime, nullable=True)
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
__table_args__ = (
|
|
Index('idx_push_schedules_enabled', 'enabled'),
|
|
)
|
|
|
|
|
|
class PushRetryJob(Base):
|
|
"""Failed push retry queue"""
|
|
__tablename__ = "push_retry_jobs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
|
|
server_name = Column(String(100), comment="冗余字段便于列表显示")
|
|
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
|
source_path = Column(String(500), nullable=False)
|
|
target_path = Column(String(500), nullable=False)
|
|
status = Column(String(20), default="pending")
|
|
retry_count = Column(Integer, default=0)
|
|
max_retries = Column(Integer, default=100)
|
|
next_retry_at = Column(DateTime)
|
|
last_error = Column(Text)
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
updated_at = Column(DateTime, default=_utcnow)
|
|
|
|
server = relationship("Server")
|
|
|
|
__table_args__ = (
|
|
Index('idx_push_retry_pending', 'status', 'next_retry_at'),
|
|
)
|
|
|
|
|
|
class AuditLog(Base):
|
|
"""Audit log — record all key operations"""
|
|
__tablename__ = "audit_logs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
admin_username = Column(String(100), nullable=True, comment="操作人用户名")
|
|
action = Column(String(50), nullable=False, comment="操作类型")
|
|
target_type = Column(String(50), nullable=True, comment="目标类型")
|
|
target_id = Column(Integer, nullable=True, comment="目标ID")
|
|
detail = Column(Text, nullable=True, comment="操作详情JSON")
|
|
ip_address = Column(String(45), nullable=True, comment="操作IP")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
__table_args__ = (
|
|
Index('idx_audit_logs_action_time', 'action', 'created_at'),
|
|
Index('idx_audit_logs_created_at', 'created_at'),
|
|
)
|
|
|
|
|
|
class Script(Base):
|
|
"""Script library — reusable shell scripts"""
|
|
__tablename__ = "scripts"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False, comment="脚本名称")
|
|
category = Column(String(50), default="ops", comment="分类: ops/deploy/check/cleanup")
|
|
content = Column(Text, nullable=False, comment="Shell命令内容")
|
|
description = Column(Text, nullable=True, comment="说明")
|
|
created_by = Column(String(100), nullable=True, comment="创建人")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
updated_at = Column(DateTime, default=_utcnow)
|
|
|
|
|
|
class ScriptExecution(Base):
|
|
"""Script execution log — results per server"""
|
|
__tablename__ = "script_executions"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True, comment="关联脚本(手动输入时为null)")
|
|
command = Column(Text, nullable=False, comment="实际执行的命令")
|
|
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
|
|
status = Column(String(20), default="pending", comment="pending/running/completed/failed")
|
|
results = Column(Text, nullable=True, comment="JSON: 每台服务器执行结果{server_id: {stdout,stderr,exit_code}}")
|
|
operator = Column(String(100), nullable=True, comment="操作人用户名")
|
|
started_at = Column(DateTime, default=_utcnow)
|
|
completed_at = Column(DateTime, nullable=True)
|
|
|
|
script = relationship("Script")
|
|
|
|
__table_args__ = (
|
|
Index('idx_script_exec_script_id', 'script_id'),
|
|
)
|
|
|
|
|
|
class DbCredential(Base):
|
|
"""Database credentials — encrypted storage for $DB_* variable substitution"""
|
|
__tablename__ = "db_credentials"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(100), nullable=False, comment="凭据名称")
|
|
db_type = Column(String(20), default="mysql", comment="数据库类型: mysql/postgresql/etc")
|
|
host = Column(String(255), default="localhost", comment="数据库主机")
|
|
port = Column(Integer, default=3306, comment="数据库端口")
|
|
username = Column(String(100), nullable=False, comment="数据库用户名")
|
|
encrypted_password = Column(String(500), nullable=False, comment="加密后的密码")
|
|
database = Column(String(100), nullable=True, comment="数据库库名")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Web SSH 会话 & 命令日志
|
|
# ──────────────────────────────────────────────
|
|
|
|
class SshSession(Base):
|
|
"""Web SSH session lifecycle tracking"""
|
|
__tablename__ = "ssh_sessions"
|
|
|
|
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID")
|
|
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
|
|
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
|
|
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
|
|
status = Column(String(20), default="active", comment="active/closed")
|
|
started_at = Column(DateTime, default=_utcnow)
|
|
closed_at = Column(DateTime, nullable=True)
|
|
|
|
server = relationship("Server")
|
|
admin = relationship("Admin")
|
|
command_logs = relationship("CommandLog", back_populates="session", cascade="all, delete-orphan")
|
|
|
|
__table_args__ = (
|
|
Index('idx_ssh_sessions_server_id', 'server_id'),
|
|
Index('idx_ssh_sessions_admin_id', 'admin_id'),
|
|
)
|
|
|
|
|
|
class CommandLog(Base):
|
|
"""SSH command log — record every command executed in terminal"""
|
|
__tablename__ = "command_logs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
session_id = Column(String(36), ForeignKey("ssh_sessions.id", ondelete="CASCADE"), nullable=True, comment="SSH会话ID")
|
|
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
|
|
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
|
|
command = Column(String(2000), nullable=False, comment="执行的命令")
|
|
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
|
|
session = relationship("SshSession", back_populates="command_logs")
|
|
server = relationship("Server")
|
|
admin = relationship("Admin")
|
|
|
|
__table_args__ = (
|
|
Index('idx_cmdlog_srv_time', 'server_id', 'created_at'),
|
|
Index('idx_cmdlog_session_id', 'session_id'),
|
|
) |