fix(tool-guard): friendly duplicate rule-id rejection and client-side name/pattern validation (#88)

This commit is contained in:
matevip 2026-05-13 09:55:30 +08:00
parent aed559d1cf
commit 2bca55f5b4
5 changed files with 36 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import vip.mate.approval.ApprovalWorkflowService; import vip.mate.approval.ApprovalWorkflowService;
import vip.mate.common.result.R; import vip.mate.common.result.R;
@ -97,8 +98,12 @@ public class SecurityController {
public R<ToolGuardRuleEntity> createRule(@RequestBody ToolGuardRuleEntity rule) { public R<ToolGuardRuleEntity> createRule(@RequestBody ToolGuardRuleEntity rule) {
try { try {
return R.ok(ruleService.createRule(rule)); return R.ok(ruleService.createRule(rule));
} catch (Exception e) { } catch (IllegalArgumentException e) {
return R.fail(e.getMessage()); return R.fail(e.getMessage());
} catch (DuplicateKeyException e) {
// Race fallback: pre-check passed but a concurrent insert took the slot.
String ruleId = rule != null && rule.getRuleId() != null ? rule.getRuleId().trim() : "";
return R.fail("Rule ID already exists: " + ruleId);
} }
} }

View File

@ -74,6 +74,12 @@ public class ToolGuardRuleService {
rule.setName(rule.getName().trim()); rule.setName(rule.getName().trim());
rule.setPattern(rule.getPattern().trim()); rule.setPattern(rule.getPattern().trim());
rule.setBuiltin(false); rule.setBuiltin(false);
// Pre-check uniqueness so the API returns a friendly message instead of
// surfacing the raw JDBC UNIQUE-constraint violation through the global
// exception handler. The DB constraint still guards against races.
if (getByRuleId(rule.getRuleId()) != null) {
throw new IllegalArgumentException("Rule ID already exists: " + rule.getRuleId());
}
ruleMapper.insert(rule); ruleMapper.insert(rule);
ruleRegistry.reload(); ruleRegistry.reload();
return rule; return rule;

View File

@ -1368,6 +1368,9 @@ export default {
}, },
messages: { messages: {
ruleIdRequired: 'Rule ID is required', ruleIdRequired: 'Rule ID is required',
nameRequired: 'Name is required',
patternRequired: 'Regex pattern is required',
ruleIdDuplicate: 'Rule ID already exists',
saveFailed: 'Failed to save rule', saveFailed: 'Failed to save rule',
}, },
}, },

View File

@ -1266,6 +1266,9 @@ export default {
}, },
messages: { messages: {
ruleIdRequired: '规则 ID 不能为空', ruleIdRequired: '规则 ID 不能为空',
nameRequired: '名称不能为空',
patternRequired: '正则模式不能为空',
ruleIdDuplicate: '规则 ID 已存在',
saveFailed: '保存规则失败', saveFailed: '保存规则失败',
}, },
}, },

View File

@ -155,12 +155,12 @@
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ t('security.toolGuard.fields.name') }}</label> <label>{{ t('security.toolGuard.fields.name') }} <span class="required">*</span></label>
<input v-model="ruleForm.name" class="form-input" /> <input v-model="ruleForm.name" class="form-input" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ t('security.toolGuard.fields.pattern') }}</label> <label>{{ t('security.toolGuard.fields.pattern') }} <span class="required">*</span></label>
<input v-model="ruleForm.pattern" class="form-input mono" placeholder="regex pattern" /> <input v-model="ruleForm.pattern" class="form-input mono" placeholder="regex pattern" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ t('security.toolGuard.fields.severity') }}</label> <label>{{ t('security.toolGuard.fields.severity') }}</label>
@ -360,6 +360,14 @@ async function saveRule() {
ElMessage.error(t('security.toolGuard.messages.ruleIdRequired')) ElMessage.error(t('security.toolGuard.messages.ruleIdRequired'))
return return
} }
if (!ruleForm.name.trim()) {
ElMessage.error(t('security.toolGuard.messages.nameRequired'))
return
}
if (!ruleForm.pattern.trim()) {
ElMessage.error(t('security.toolGuard.messages.patternRequired'))
return
}
try { try {
if (editingRule.value) { if (editingRule.value) {
@ -370,7 +378,12 @@ async function saveRule() {
showRuleModal.value = false showRuleModal.value = false
loadRules() loadRules()
} catch (e: any) { } catch (e: any) {
ElMessage.error(e?.msg || e?.message || t('security.toolGuard.messages.saveFailed')) const raw = e?.msg || e?.message || ''
if (typeof raw === 'string' && raw.toLowerCase().includes('already exists')) {
ElMessage.error(t('security.toolGuard.messages.ruleIdDuplicate'))
} else {
ElMessage.error(raw || t('security.toolGuard.messages.saveFailed'))
}
} }
} }