From 9ac8e3604ce2428abb55f56363e6c9ff1556d6c4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:52:18 +0200 Subject: [PATCH] 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) --- .../ClaimMappingAdminController.java | 28 +++++++++++++++---- ui/src/api/queries/admin/claim-mappings.ts | 14 ++++++++-- ui/src/pages/Admin/ClaimMappingRulesModal.tsx | 23 +++++++-------- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java index a6e6f191..5003b04d 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java @@ -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 matchedRules, List effectiveRoles, List effectiveGroups, boolean fallback) {} + record TestRuleRequest(String id, String claim, String matchType, String matchValue, + String action, String target, int priority) {} + + record TestRequest(List rules, Map claims) {} + @PostMapping("/test") - @Operation(summary = "Test claim mapping rules against a set of claims") - public TestResponse test(@RequestBody Map claims) { - List rules = repository.findAll(); - List 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 idLookup = new HashMap<>(); + List 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 results = claimMappingService.evaluate(rules, request.claims()); List 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(); diff --git a/ui/src/api/queries/admin/claim-mappings.ts b/ui/src/api/queries/admin/claim-mappings.ts index 8f32e417..8544b800 100644 --- a/ui/src/api/queries/admin/claim-mappings.ts +++ b/ui/src/api/queries/admin/claim-mappings.ts @@ -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) => + mutationFn: ({ rules, claims }: { rules: TestRuleInput[]; claims: Record }) => adminFetch('/claim-mappings/test', { method: 'POST', - body: JSON.stringify(claims), + body: JSON.stringify({ rules, claims }), }), }); } diff --git a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx index 33a663c8..dde57ec8 100644 --- a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx +++ b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx @@ -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) { {actionLabel(rule.action)} {rule.target} - {isMatched ? ( - - ) : ( -
- - - - -
- )} +
+ {isMatched && } + + + + +
)}