Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/cli/guards.py
T
insistence 2a055ba648 feat: 新增插件系统 (#112)
* feat: 初始化插件系统

* refactor: 收口插件系统运行时重构

* perf: 优化插件系统类型提示

* fix&perf: 修复和优化插件系统

* fix: 修复gitignore规则误忽略插件文件的问题

* fix: 修复运行时插件根路径算错的问题

* fix: 加强插件发现和路由注册的防护措施

* revert: 回滚定时任务白名单

* fix: 移除未使用的应用路由注册探测

* revert: 恢复部分代码

* perf: 优化插件系统

* docs: 新增插件开发文档

* perf: 优化插件管理模块

* perf: 提升插件系统核心能力

* refactor: 重构生命周期 step runner

* fix: 修复lint错误

* test: 清理测试用例

* test: 调整测试目录名称

* fix: 修复前后端目录硬编码的问题

* fix: 修复插件系统安全性缺口

* refactor: 重新设计插件生命周期 Migration 事务与回滚

* perf: 优化插件系统边界问题

* refactor: 重构当前插件系统的依赖体系设计

* perf: 优化代码

* perf: 优化代码

* fix: 修复代码合并问题

* fix: 修复bug

* perf: 优化代码

* perf&fix: 优化代码和修复bug

* docs: 优化文档格式

* feat: 适配Vue2版本

* docs: 更新README文档

* fix: 修复ruff lint错误

* chore: 更新后端依赖文件
2026-07-28 20:35:18 +08:00

250 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
from dataclasses import dataclass, field
from typing import Literal
import click
import typer
from cli.context import CliContext
from cli.exit_codes import GUARD_REJECTED
from cli.output import CommandResult
DangerousCommandRiskLevel = Literal['high', 'normal']
@dataclass(frozen=True)
class DangerousCommandRule:
"""
危险命令保护规则。
:param command_name: 命令唯一标识
:param risk_level: 风险级别
:param supports_dry_run: 是否支持演练执行
"""
command_name: str
risk_level: DangerousCommandRiskLevel
supports_dry_run: bool
@dataclass(frozen=True)
class DangerousCommandRuleRegistry:
"""
危险命令规则注册表。
该注册表负责维护 CLI 内所有危险命令的风险元数据,并提供查询与
强制获取能力,作为上下文工厂、风险元数据与测试的统一入口。
:param rules: 按命令名索引的危险命令规则表
"""
rules: dict[str, DangerousCommandRule]
def get_rule(self, command_name: str) -> DangerousCommandRule | None:
"""
获取指定命令的危险命令规则。
:param command_name: 命令唯一标识
:return: 命令规则,不存在时返回 None
"""
return self.rules.get(command_name)
def require_rule(self, command_name: str) -> DangerousCommandRule:
"""
获取指定命令的危险命令规则,不存在时抛出异常。
:param command_name: 命令唯一标识
:return: 命令规则
:raises ValueError: 命令未注册危险命令规则时抛出
"""
rule = self.get_rule(command_name)
if rule is None:
raise ValueError(f'危险命令未注册保护规则:{command_name}')
return rule
@dataclass(frozen=True)
class DangerousCommandResultBuilder:
"""
危险命令结果构建器。
该对象负责统一构建危险命令拒绝结果,避免保护服务内部继续拼装
结构化负载细节。
"""
@staticmethod
def build_guard_reject_result(message: str, hint: str) -> CommandResult:
"""
构建危险命令拒绝结果。
:param message: 拒绝原因
:param hint: 补充提示
:return: 命令拒绝结果
"""
return CommandResult(
data={
'ok': False,
'message': message,
'hint': hint,
},
exit_code=GUARD_REJECTED,
)
@dataclass(frozen=True)
class DangerousCommandConfirmationService:
"""
危险命令确认服务。
该对象负责处理 TTY 检测、确认提示与交互取消异常收口。
:param result_builder: 危险命令结果构建器
"""
result_builder: DangerousCommandResultBuilder
def confirm(self, ctx: CliContext, *, command_name: str) -> CommandResult | None:
"""
执行危险命令交互确认。
:param ctx: CLI 上下文
:param command_name: 命令唯一标识
:return: 拒绝结果或 None
"""
if ctx.yes:
return None
if not sys.stdin.isatty():
return self.result_builder.build_guard_reject_result(
f'已取消危险命令执行:{command_name}',
'当前命令需要交互确认;如需非交互执行,请传入 --yes',
)
try:
confirmed = typer.confirm(
f'确认执行危险命令 `{command_name}` 吗? 当前环境:{ctx.env}{"dry-run" if ctx.dry_run else ""}',
default=False,
)
except (click.Abort, EOFError, KeyboardInterrupt):
return self.result_builder.build_guard_reject_result(
f'已取消危险命令执行:{command_name}',
'当前命令需要交互确认;如需非交互执行,请传入 --yes',
)
if confirmed:
return None
return self.result_builder.build_guard_reject_result(
f'已取消危险命令执行:{command_name}',
'如需跳过确认,请传入 --yes',
)
@dataclass
class DangerousCommandGuardService:
"""
危险命令保护执行服务。
该服务负责根据命令规则和 CLI 上下文执行生产环境保护、交互确认
与拒绝结果收口。
:param rule_registry: 危险命令规则注册表
:param result_builder: 危险命令结果构建器
:param confirmation_service: 危险命令确认服务
"""
rule_registry: DangerousCommandRuleRegistry
result_builder: DangerousCommandResultBuilder = field(default_factory=DangerousCommandResultBuilder)
confirmation_service: DangerousCommandConfirmationService = field(init=False)
def __post_init__(self) -> None:
"""
初始化危险命令保护服务依赖。
:return: None
"""
self.confirmation_service = DangerousCommandConfirmationService(self.result_builder)
def guard(self, ctx: CliContext, *, rule: DangerousCommandRule) -> CommandResult | None:
"""
执行危险命令保护与确认。
:param ctx: CLI 上下文
:param rule: 危险命令规则
:return: 拒绝结果或 None
"""
command_name = rule.command_name
if ctx.env == 'prod' and not ctx.allow_prod:
return self.result_builder.build_guard_reject_result(
f'生产环境默认禁止直接执行危险命令:{command_name}',
'如确认执行,请传入 --allow-prod;如需跳过确认,请同时传入 --yes',
)
return self.confirmation_service.confirm(ctx, command_name=command_name)
DEFAULT_DANGEROUS_COMMAND_RULES: dict[str, DangerousCommandRule] = {
'cache clear': DangerousCommandRule(command_name='cache clear', risk_level='high', supports_dry_run=True),
'cache warmup': DangerousCommandRule(command_name='cache warmup', risk_level='normal', supports_dry_run=False),
'db upgrade': DangerousCommandRule(command_name='db upgrade', risk_level='high', supports_dry_run=True),
'db init': DangerousCommandRule(command_name='db init', risk_level='high', supports_dry_run=True),
'db downgrade': DangerousCommandRule(command_name='db downgrade', risk_level='high', supports_dry_run=True),
'db revision': DangerousCommandRule(command_name='db revision', risk_level='high', supports_dry_run=True),
'config set': DangerousCommandRule(command_name='config set', risk_level='high', supports_dry_run=True),
'config sync-cache': DangerousCommandRule(
command_name='config sync-cache',
risk_level='normal',
supports_dry_run=False,
),
'crypto rotate': DangerousCommandRule(command_name='crypto rotate', risk_level='high', supports_dry_run=True),
'job run-once': DangerousCommandRule(command_name='job run-once', risk_level='normal', supports_dry_run=False),
'job pause': DangerousCommandRule(command_name='job pause', risk_level='normal', supports_dry_run=False),
'job resume': DangerousCommandRule(command_name='job resume', risk_level='normal', supports_dry_run=False),
'job sync': DangerousCommandRule(command_name='job sync', risk_level='normal', supports_dry_run=False),
'gen import-table': DangerousCommandRule(command_name='gen import-table', risk_level='high', supports_dry_run=True),
'gen create-table': DangerousCommandRule(command_name='gen create-table', risk_level='high', supports_dry_run=True),
'gen export': DangerousCommandRule(command_name='gen export', risk_level='high', supports_dry_run=True),
'gen sync-db': DangerousCommandRule(command_name='gen sync-db', risk_level='normal', supports_dry_run=False),
'plugin install': DangerousCommandRule(command_name='plugin install', risk_level='high', supports_dry_run=True),
'plugin install-deps': DangerousCommandRule(
command_name='plugin install-deps',
risk_level='high',
supports_dry_run=True,
),
'plugin upgrade': DangerousCommandRule(command_name='plugin upgrade', risk_level='high', supports_dry_run=True),
'plugin batch': DangerousCommandRule(command_name='plugin batch', risk_level='high', supports_dry_run=True),
'plugin enable': DangerousCommandRule(command_name='plugin enable', risk_level='normal', supports_dry_run=True),
'plugin disable': DangerousCommandRule(command_name='plugin disable', risk_level='normal', supports_dry_run=True),
'plugin config set': DangerousCommandRule(
command_name='plugin config set',
risk_level='normal',
supports_dry_run=False,
),
'plugin config import': DangerousCommandRule(
command_name='plugin config import',
risk_level='normal',
supports_dry_run=False,
),
'plugin config export': DangerousCommandRule(
command_name='plugin config export',
risk_level='normal',
supports_dry_run=False,
),
'plugin uninstall': DangerousCommandRule(
command_name='plugin uninstall', risk_level='normal', supports_dry_run=True
),
'plugin purge': DangerousCommandRule(command_name='plugin purge', risk_level='high', supports_dry_run=True),
'plugin mark-success': DangerousCommandRule(
command_name='plugin mark-success',
risk_level='high',
supports_dry_run=False,
),
'plugin mark-failed': DangerousCommandRule(
command_name='plugin mark-failed',
risk_level='high',
supports_dry_run=False,
),
}
DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY = DangerousCommandRuleRegistry(rules=DEFAULT_DANGEROUS_COMMAND_RULES)
DEFAULT_DANGEROUS_COMMAND_GUARD = DangerousCommandGuardService(rule_registry=DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY)