78f60c6857
P0 修复: - /api/ready import 错误 (_get_engine + settings.create_redis_client) - 删 agent.otp_secret/otp_enabled 双字段 (migration 026) - 重建 021_rbac migration (IF NOT EXISTS 兼容) P1 新增: - 企微 SSO (auth_wecom_sso.py, useWeChatWorkSSO composable, PortalSelect UA 检测) - RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围 (rbac_service + seed_rbac + require_permission) - audit_log 模型 + migration 027 + 服务 + API - 管理后台 RBAC 权限矩阵 UI (PermissionsMatrix.vue) 质量: - pytest 405 passed / 33 pre-existing failed / 4 xfailed (v0.7.1 引入失败 = 0) - conftest GBK patch 强制 UTF-8 读 .env - .gitignore 排除 *.b64 (含 admin token 凭据) - DEPLOY-v0.7.1.md 7 步 runbook + 4 坑 + 回滚预案
81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""audit_logs 表 — 高危操作/登录/MFA 审计日志
|
|
|
|
Revision ID: 027_audit_logs
|
|
Revises: 026_drop_agent_otp_legacy
|
|
Create Date: 2026-06-22 (v0.7.1)
|
|
|
|
v0.7.1 task #89 实施,配合 RBAC 5 角色的 audit_log 资源(给 auditor 角色只读用)
|
|
|
|
字段:
|
|
- id: UUID 主键
|
|
- employee_id: 操作人(企微 UserID / 'system')
|
|
- action: 操作类型
|
|
- resource: 目标资源类型
|
|
- resource_id: 目标资源 ID
|
|
- details: JSON 详细上下文
|
|
- result: success / failure / partial
|
|
- ip_address: 来源 IP
|
|
- user_agent: 来源 UA
|
|
- created_at: 时间
|
|
|
|
索引:
|
|
- idx_audit_employee_id: 按操作人查
|
|
- idx_audit_action: 按操作类型查
|
|
- idx_audit_resource: 按资源类型+ID 查
|
|
- idx_audit_created_at: 按时间范围查(默认倒序)
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers
|
|
revision = '027_audit_logs'
|
|
down_revision = '026_drop_agent_otp_legacy'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""建 audit_logs 表 + 索引。"""
|
|
bind = op.get_bind()
|
|
inspector = sa.inspect(bind)
|
|
|
|
if not inspector.has_table('audit_logs'):
|
|
op.create_table(
|
|
'audit_logs',
|
|
sa.Column('id', sa.String(36), primary_key=True),
|
|
sa.Column('employee_id', sa.String(100), nullable=False,
|
|
comment='操作人(employee_id / system)'),
|
|
sa.Column('action', sa.String(50), nullable=False,
|
|
comment='操作类型'),
|
|
sa.Column('resource', sa.String(50), nullable=False,
|
|
comment='目标资源类型'),
|
|
sa.Column('resource_id', sa.String(100), nullable=True,
|
|
comment='目标资源 ID'),
|
|
sa.Column('details', sa.JSON, nullable=True,
|
|
comment='详细上下文(JSON)'),
|
|
sa.Column('result', sa.String(20), nullable=False, server_default='success',
|
|
comment='执行结果'),
|
|
sa.Column('ip_address', sa.String(64), nullable=True,
|
|
comment='来源 IP'),
|
|
sa.Column('user_agent', sa.Text, nullable=True,
|
|
comment='来源 User-Agent'),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
|
|
comment='时间'),
|
|
)
|
|
|
|
# 4 个索引 (IF NOT EXISTS 兼容)
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_audit_employee_id ON audit_logs (employee_id)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs (action)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_audit_resource ON audit_logs (resource, resource_id)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs (created_at)")
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""删 audit_logs 表(顺序: 删索引 → 删表)。"""
|
|
op.execute("DROP INDEX IF EXISTS idx_audit_created_at")
|
|
op.execute("DROP INDEX IF EXISTS idx_audit_resource")
|
|
op.execute("DROP INDEX IF EXISTS idx_audit_action")
|
|
op.execute("DROP INDEX IF EXISTS idx_audit_employee_id")
|
|
op.execute("DROP TABLE IF EXISTS audit_logs")
|