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 && } + + + + +
)}