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