From 58e802e2d44e89142ea0a986751aceb9904c8db1 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:12:39 +0200 Subject: [PATCH] feat: close modal on successful apply, update design spec Modal auto-closes after Apply succeeds. Design spec updated to reflect implemented behavior: local-edit-then-apply pattern, target select dropdowns, amber pill for add-to-group, close-on-success. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...04-14-claim-mapping-rules-editor-design.md | 166 ++++++++++++++++++ ui/src/pages/Admin/ClaimMappingRulesModal.tsx | 2 +- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-04-14-claim-mapping-rules-editor-design.md diff --git a/docs/superpowers/specs/2026-04-14-claim-mapping-rules-editor-design.md b/docs/superpowers/specs/2026-04-14-claim-mapping-rules-editor-design.md new file mode 100644 index 00000000..8b5dd15a --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-claim-mapping-rules-editor-design.md @@ -0,0 +1,166 @@ +# Claim Mapping Rules Editor + +## Problem + +The server has a claim mapping rules engine (`ClaimMappingService`) that maps arbitrary JWT claims to Cameleer roles and groups during OIDC login. The backend API exists (`/api/v1/admin/claim-mappings` CRUD), but there is no UI for managing these rules. Admins must use curl or Swagger to create, edit, and delete rules. + +## Solution + +Add a rules editor modal to the existing OIDC config page (`/admin/oidc`), triggered from the Claim Mapping section. The modal contains a compact rules table with inline CRUD and a collapsible server-side test panel. + +## Design + +### Trigger + +An "Edit Rules" button added to the bottom of the existing Claim Mapping section on the OIDC config page. Displays a badge with the active rule count (e.g. "3 active rules"). Visible in both read and edit mode of the OIDC form — the rules modal manages its own state independently of the OIDC form's edit/save lifecycle. + +### Modal — Rules Table + +A full-width modal dialog with the title "Claim Mapping Rules" and a subtitle explaining their purpose. + +**Table columns:** + +| Column | Content | Width | +|--------|---------|-------| +| `#` | Priority number (display only, derived from order) | 30px | +| Claim | Claim name in monospace code style | auto | +| Match | Match type pill: `equals`, `contains`, `regex` | auto | +| Value | Match value in monospace | auto | +| Action | Action pill: `assign role` (green/success), `add to group` (amber) | auto | +| Target | Role name or group name | auto | +| Actions | Reorder arrows + edit pencil + delete x | ~80px | + +**Action icons per row:** + +- Up/down arrow buttons for reordering. Up hidden on first row, down hidden on last row. Moving a rule swaps priorities locally (saved to server on Apply). +- Pencil icon switches the row to inline edit mode (same field layout as the add row). +- X icon deletes the rule locally after a `ConfirmDialog` confirmation (saved to server on Apply). + +**Inline add form:** + +A row at the bottom of the table with input fields matching the table columns: + +- `Input` — claim name (text, placeholder "Claim name") +- `Select` — match type (equals / contains / regex) +- `Input` — match value (text, placeholder "Match value") +- `Select` — action (assign role / add to group) +- `Select` — target, populated from existing roles (when action is "assign role") or existing groups (when action is "add to group"). Includes a placeholder option ("Select role..." / "Select group..."). Switching action resets the target selection. +- `Button` — "+ Add" (disabled until claim, value, and target are filled) + +New rules are assigned a priority one higher than the current maximum. + +**Inline edit mode:** + +When the pencil icon is clicked, the row's static cells become editable inputs (same controls as the add form, including the target Select dropdown). The action icons change to a checkmark (save) and x (cancel). Saving updates local state only — persisted to server on Apply. + +**Empty state:** + +When no rules exist, show a centered message: "No claim mapping rules configured. Rules let you assign roles and groups based on JWT claims." with the add form below. + +### Modal — Test Panel + +A collapsible section at the bottom of the modal, separated by a heavier border. Collapsed by default. Toggle via a clickable header row showing "Test Rules — Paste a decoded JWT to preview which rules would fire". + +**Layout when expanded:** + +Side-by-side split: + +- **Left:** `textarea` for pasting decoded JWT claims as JSON. Placeholder shows a sample JSON object. +- **Right:** Results panel with: + - Each matched rule listed: rule number, claim name, match description, arrow, action and target + - "Effective" summary line: combined roles and groups + - If no rules matched: "No rules matched — would fall back to default roles: [roles]" + +**"Test" button** below the textarea triggers `POST /api/v1/admin/claim-mappings/test`. The evaluation runs server-side using `ClaimMappingService.evaluate()` — the same code path as the production OIDC login flow in `OidcAuthController.applyClaimMappings()`. + +**Visual feedback:** While test results are displayed, matched rows in the table above get a subtle green background tint and a checkmark in place of their action icons. + +### Local Edit + Apply Pattern + +All changes (add, edit, delete, reorder) modify local state only. No API calls are made until the admin clicks Apply. + +- **On modal open:** server rules are cloned into local state. +- **Cancel:** discards all local changes and closes the modal. +- **Apply:** diffs local state against server state — creates new rules (temp IDs), updates changed rules, deletes removed rules. On success, shows a toast and closes the modal. On failure, shows an error toast and keeps the modal open. + +### Reordering + +Up/down arrow buttons on each row. Clicking an arrow swaps the priority values of two adjacent rules in local state. + +Priority is a server-side integer. The UI displays it as the row number (`#` column) — admins never edit the number directly. + +### New Backend Endpoint + +``` +POST /api/v1/admin/claim-mappings/test +Content-Type: application/json +Authorization: Bearer + +{ + "sub": "user-42", + "email": "jane@acme.com", + "department": "engineering", + "groups": ["frontend", "design"] +} + +Response 200: +{ + "matchedRules": [ + { + "ruleId": "uuid", + "priority": 1, + "claim": "email", + "matchType": "regex", + "matchValue": ".*@acme\\.com$", + "action": "assignRole", + "target": "OPERATOR" + } + ], + "effectiveRoles": ["OPERATOR"], + "effectiveGroups": [], + "fallback": false +} +``` + +The endpoint: + +- Requires ADMIN role (same as other claim mapping endpoints) +- Loads all rules from the database +- Calls `ClaimMappingService.evaluate(rules, claims)` — production code path +- Maps `MappingResult` list to the response DTO +- Returns `fallback: true` when no rules matched (UI shows default roles message) +- Read-only — no side effects, no user creation, no role assignment + +### Error Handling + +- **Invalid JSON in test textarea:** Show inline validation error, disable Test button +- **API errors on CRUD:** Toast notifications (consistent with rest of admin UI) +- **ConfirmDialog on delete:** Type-to-confirm not needed for rules (they're low-risk, easily recreated). Simple "Delete this rule?" confirm/cancel. + +## Files to Create/Modify + +### Backend + +- `ClaimMappingAdminController.java` — add `POST /test` endpoint +- New DTO for test response (inline record or separate class) + +### Frontend + +- New component: `ClaimMappingRulesModal.tsx` — the modal with table, inline CRUD, and test panel +- New CSS module: `ClaimMappingRulesModal.module.css` +- New API hooks: `useClaimMappingRules()`, `useCreateRule()`, `useUpdateRule()`, `useDeleteRule()`, `useTestRules()` — React Query hooks for the claim mapping API +- Modify: `OidcConfigPage.tsx` — add "Edit Rules" button in the Claim Mapping section, render the modal + +### Not in Scope + +- Drag-and-drop reordering (up/down arrows are sufficient for 1-10 rules) +- Groups Claim field (rules can already match any claim including group claims) +- Bulk import/export of rules +- Rule templates or presets + +## Visual Reference + +Mockups available in `.superpowers/brainstorm/172364-1776174996/content/`: + +- `claim-rules-modal.html` — initial layout (trigger button + table) +- `claim-rules-modal-v2.html` — full modal with test panel expanded and match highlighting diff --git a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx index 2771e3a4..33a663c8 100644 --- a/ui/src/pages/Admin/ClaimMappingRulesModal.tsx +++ b/ui/src/pages/Admin/ClaimMappingRulesModal.tsx @@ -229,8 +229,8 @@ export default function ClaimMappingRulesModal({ open, onClose }: Props) { } } - setDirty(false); toast({ title: 'Rules saved', variant: 'success' }); + handleClose(); } catch (e: any) { toast({ title: 'Failed to save rules', description: e.message, variant: 'error', duration: 86_400_000 }); } finally {