diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java index 77d18328..156e5935 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/controller/SecurityController.java @@ -138,6 +138,17 @@ public class SecurityController { } } + @Operation(summary = "按主键 ID 删除自定义规则(兜底,rule_id 异常时使用)") + @DeleteMapping("/guard/rules/by-id/{id}") + public R deleteRuleByPk(@PathVariable Long id) { + try { + ruleService.deleteRuleByPk(id); + return R.ok("删除成功"); + } catch (IllegalArgumentException e) { + return R.fail(e.getMessage()); + } + } + // ==================== Audit ==================== @Operation(summary = "审计日志") diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleService.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleService.java index 9dfdbe4b..a53cbff4 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleService.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleService.java @@ -64,10 +64,15 @@ public class ToolGuardRuleService { * 新增自定义规则 */ public ToolGuardRuleEntity createRule(ToolGuardRuleEntity rule) { - if (rule == null || rule.getRuleId() == null || rule.getRuleId().isBlank()) { - throw new IllegalArgumentException("Rule ID is required"); + if (rule == null) { + throw new IllegalArgumentException("Rule body is required"); } + requireNonBlank(rule.getRuleId(), "Rule ID"); + requireNonBlank(rule.getName(), "Rule name"); + requireNonBlank(rule.getPattern(), "Rule pattern"); rule.setRuleId(rule.getRuleId().trim()); + rule.setName(rule.getName().trim()); + rule.setPattern(rule.getPattern().trim()); rule.setBuiltin(false); ruleMapper.insert(rule); ruleRegistry.reload(); @@ -75,7 +80,8 @@ public class ToolGuardRuleService { } /** - * 更新规则 + * 更新规则。仅覆盖请求里显式提供的字段;显式传入的关键字段(name / pattern) + * 不允许置为空白,避免回写出无意义的"空名空模式"行。 */ public ToolGuardRuleEntity updateRule(String ruleId, ToolGuardRuleEntity update) { ToolGuardRuleEntity existing = getByRuleId(ruleId); @@ -83,14 +89,20 @@ public class ToolGuardRuleService { throw new IllegalArgumentException("Rule not found: " + ruleId); } - if (update.getName() != null) existing.setName(update.getName()); + if (update.getName() != null) { + requireNonBlank(update.getName(), "Rule name"); + existing.setName(update.getName().trim()); + } if (update.getDescription() != null) existing.setDescription(update.getDescription()); if (update.getToolName() != null) existing.setToolName(update.getToolName()); if (update.getParamName() != null) existing.setParamName(update.getParamName()); if (update.getCategory() != null) existing.setCategory(update.getCategory()); if (update.getSeverity() != null) existing.setSeverity(update.getSeverity()); if (update.getDecision() != null) existing.setDecision(update.getDecision()); - if (update.getPattern() != null) existing.setPattern(update.getPattern()); + if (update.getPattern() != null) { + requireNonBlank(update.getPattern(), "Rule pattern"); + existing.setPattern(update.getPattern().trim()); + } if (update.getExcludePattern() != null) existing.setExcludePattern(update.getExcludePattern()); if (update.getRemediation() != null) existing.setRemediation(update.getRemediation()); if (update.getEnabled() != null) existing.setEnabled(update.getEnabled()); @@ -101,6 +113,12 @@ public class ToolGuardRuleService { return existing; } + private static void requireNonBlank(String value, String fieldLabel) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldLabel + " is required"); + } + } + /** * 启用/禁用规则 */ @@ -128,4 +146,24 @@ public class ToolGuardRuleService { ruleMapper.deleteById(existing.getId()); ruleRegistry.reload(); } + + /** + * 按主键 ID 删除自定义规则。兜底通道:当 rule_id 因历史脏数据为空或无法走 + * /guard/rules/{ruleId} 路径变量时,UI 仍可通过主键删除。 + */ + public void deleteRuleByPk(Long id) { + if (id == null) { + throw new IllegalArgumentException("Rule primary key is required"); + } + ToolGuardRuleEntity existing = ruleMapper.selectById(id); + if (existing == null) { + throw new IllegalArgumentException("Rule not found: id=" + id); + } + if (Boolean.TRUE.equals(existing.getBuiltin())) { + throw new IllegalArgumentException( + "Cannot delete builtin rule: " + existing.getRuleId()); + } + ruleMapper.deleteById(id); + ruleRegistry.reload(); + } } diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V101__cleanup_blank_tool_guard_rule_id.sql b/mateclaw-server/src/main/resources/db/migration/h2/V101__cleanup_blank_tool_guard_rule_id.sql new file mode 100644 index 00000000..49954404 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V101__cleanup_blank_tool_guard_rule_id.sql @@ -0,0 +1,26 @@ +-- Earlier releases let the rule-create API persist a custom guard rule +-- with a blank rule_id because the service skipped the not-blank check. +-- The resulting row was undeletable from the UI: the delete endpoint is +-- /guard/rules/{ruleId}, and a blank path variable produces a 404 instead +-- of resolving to the row. This migration does two things: +-- +-- 1. Purge any orphan rows already persisted on existing installations +-- so users who hit the bug on v1.2.0 can recover without direct DB +-- surgery. NULL, empty, and whitespace-only rule_id are all swept; +-- built-in rules are excluded defensively because they are seeded +-- with stable IDs and should never appear here. +-- +-- 2. Add a CHECK constraint so the database itself rejects blank +-- rule_id going forward. The service-layer guard already prevents +-- this from the UI, but the DB constraint defends against any +-- future code path that bypasses the service (batch import, direct +-- SQL, future endpoints) and makes the invariant explicit at the +-- schema level. + +DELETE FROM mate_tool_guard_rule +WHERE (rule_id IS NULL OR LENGTH(TRIM(rule_id)) = 0) + AND (builtin IS NULL OR builtin = FALSE); + +ALTER TABLE mate_tool_guard_rule + ADD CONSTRAINT ck_tool_guard_rule_id_nonblank + CHECK (rule_id IS NOT NULL AND LENGTH(TRIM(rule_id)) > 0); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V101__cleanup_blank_tool_guard_rule_id.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V101__cleanup_blank_tool_guard_rule_id.sql new file mode 100644 index 00000000..49960ed6 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V101__cleanup_blank_tool_guard_rule_id.sql @@ -0,0 +1,14 @@ +-- See the matching H2 file for context. This migration purges any +-- orphan rows that earlier releases persisted with a blank rule_id and +-- then installs a CHECK constraint so the schema itself rejects blank +-- rule_id, defending against any future code path that bypasses the +-- service-layer guard. CHECK constraints are enforced from MySQL 8.0.16 +-- onward; this project targets MySQL 8.0+ so the constraint is live. + +DELETE FROM mate_tool_guard_rule +WHERE (rule_id IS NULL OR LENGTH(TRIM(rule_id)) = 0) + AND (builtin IS NULL OR builtin = FALSE); + +ALTER TABLE mate_tool_guard_rule + ADD CONSTRAINT ck_tool_guard_rule_id_nonblank + CHECK (rule_id IS NOT NULL AND LENGTH(TRIM(rule_id)) > 0);