fix: allow testing claim mapping rules before saving and keep rows editable after test
The test endpoint now accepts inline rules from the client instead of reading from the database, so unsaved rules can be tested. Matched rows show the checkmark alongside action buttons instead of replacing them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -80,21 +81,36 @@ public class ClaimMappingAdminController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
record MatchedRuleResponse(UUID ruleId, int priority, String claim, String matchType,
|
||||
record MatchedRuleResponse(String ruleId, int priority, String claim, String matchType,
|
||||
String matchValue, String action, String target) {}
|
||||
|
||||
record TestResponse(List<MatchedRuleResponse> matchedRules, List<String> effectiveRoles,
|
||||
List<String> effectiveGroups, boolean fallback) {}
|
||||
|
||||
record TestRuleRequest(String id, String claim, String matchType, String matchValue,
|
||||
String action, String target, int priority) {}
|
||||
|
||||
record TestRequest(List<TestRuleRequest> rules, Map<String, Object> claims) {}
|
||||
|
||||
@PostMapping("/test")
|
||||
@Operation(summary = "Test claim mapping rules against a set of claims")
|
||||
public TestResponse test(@RequestBody Map<String, Object> claims) {
|
||||
List<ClaimMappingRule> rules = repository.findAll();
|
||||
List<ClaimMappingService.MappingResult> results = claimMappingService.evaluate(rules, claims);
|
||||
@Operation(summary = "Test claim mapping rules against a set of claims (accepts unsaved rules)")
|
||||
public TestResponse test(@RequestBody TestRequest request) {
|
||||
// Build a lookup from synthetic UUID → original string ID (supports temp- prefixed IDs)
|
||||
Map<UUID, String> idLookup = new HashMap<>();
|
||||
List<ClaimMappingRule> rules = request.rules().stream()
|
||||
.map(r -> {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
idLookup.put(uuid, r.id());
|
||||
return new ClaimMappingRule(uuid, r.claim(), r.matchType(), r.matchValue(),
|
||||
r.action(), r.target(), r.priority(), null);
|
||||
})
|
||||
.toList();
|
||||
|
||||
List<ClaimMappingService.MappingResult> results = claimMappingService.evaluate(rules, request.claims());
|
||||
|
||||
List<MatchedRuleResponse> matched = results.stream()
|
||||
.map(r -> new MatchedRuleResponse(
|
||||
r.rule().id(), r.rule().priority(), r.rule().claim(),
|
||||
idLookup.get(r.rule().id()), r.rule().priority(), r.rule().claim(),
|
||||
r.rule().matchType(), r.rule().matchValue(),
|
||||
r.rule().action(), r.rule().target()))
|
||||
.toList();
|
||||
|
||||
@@ -90,12 +90,22 @@ export function useDeleteClaimMappingRule() {
|
||||
});
|
||||
}
|
||||
|
||||
export interface TestRuleInput {
|
||||
id: string;
|
||||
claim: string;
|
||||
matchType: string;
|
||||
matchValue: string;
|
||||
action: string;
|
||||
target: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export function useTestClaimMappingRules() {
|
||||
return useMutation({
|
||||
mutationFn: (claims: Record<string, unknown>) =>
|
||||
mutationFn: ({ rules, claims }: { rules: TestRuleInput[]; claims: Record<string, unknown> }) =>
|
||||
adminFetch<TestResponse>('/claim-mappings/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(claims),
|
||||
body: JSON.stringify({ rules, claims }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,7 +250,11 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) {
|
||||
setTestError('Invalid JSON — paste a decoded JWT claims object');
|
||||
return;
|
||||
}
|
||||
testRules.mutate(claims, {
|
||||
const rules = localRules.map((r) => ({
|
||||
id: r.id, claim: r.claim, matchType: r.matchType,
|
||||
matchValue: r.matchValue, action: r.action, target: r.target, priority: r.priority,
|
||||
}));
|
||||
testRules.mutate({ rules, claims }, {
|
||||
onSuccess: (result) => setTestResult(result),
|
||||
onError: (e) => setTestError(e.message),
|
||||
});
|
||||
@@ -333,16 +337,13 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) {
|
||||
<td><span className={`${styles.pill} ${actionPillClass(rule.action)}`}>{actionLabel(rule.action)}</span></td>
|
||||
<td className={styles.targetCell}>{rule.target}</td>
|
||||
<td>
|
||||
{isMatched ? (
|
||||
<span className={styles.matchCheck}><Check size={14} /></span>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<button className={styles.iconBtn} onClick={() => handleMove(rule, 'up')} disabled={idx === 0} title="Move up"><ChevronUp size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => handleMove(rule, 'down')} disabled={idx === sorted.length - 1} title="Move down"><ChevronDown size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => startEdit(rule)} title="Edit"><Pencil size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => setDeleteTarget(rule)} title="Delete"><X size={14} /></button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.actions}>
|
||||
{isMatched && <span className={styles.matchCheck}><Check size={14} /></span>}
|
||||
<button className={styles.iconBtn} onClick={() => handleMove(rule, 'up')} disabled={idx === 0} title="Move up"><ChevronUp size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => handleMove(rule, 'down')} disabled={idx === sorted.length - 1} title="Move down"><ChevronDown size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => startEdit(rule)} title="Edit"><Pencil size={14} /></button>
|
||||
<button className={styles.iconBtn} onClick={() => setDeleteTarget(rule)} title="Delete"><X size={14} /></button>
|
||||
</div>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user