feat(permissions): let permissionKey express anyOf/allOf key sets - #1765
Conversation
`permissionKey` accepted exactly one `Key`, so an affordance legitimately
gated on more than one key could not adopt the declarative path at all:
supplying either key alone narrows access, and supplying none forfeits the
`PermissionShield` that explains the block.
It now also accepts a set with an explicit combinator —
`{ anyOf: Key[] }` (any one key suffices) or `{ allOf: Key[] }` (every key
required) — resolved by `useHasPermission` with `.some(...)` / `.every(...)`.
A bare `Key` takes the original code path untouched.
An empty, non-array, or double-combinator set is denied rather than granted:
a set is an explicit statement that the affordance IS gated, and
`[].every(...)` is vacuously true, so `{ allOf: [] }` would otherwise open
the affordance to everyone.
`PermissionShield` now lists every key the user is missing — "Needs any
of: A, B" / "Needs all of: A, B" plus a row and description per key — instead
of naming one. Single-key tooltips are unchanged.
`NavigationNavbar` makes an item the user cannot reach genuinely inert
rather than merely styled as disabled: it withholds both `onClick` and the
expand toggle, and forces the section collapsed. MUI enforces `disabled` on
a `<li>` MenuItem with `pointer-events` alone, so a disabled section could
still be toggled open and activated — which is how an unpermitted section
landed the user on a "you don't have permission" error page. A section is
reachable exactly when one of its children is, i.e. `{ anyOf: [...child
keys...] }`.
Fixes #1747
Signed-off-by: Alex Quinn <227241865+alexquincy@users.noreply.github.com>
📝 WalkthroughWalkthroughThis PR adds ChangesPermission key sets
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant NavigationNavbarItem
participant useHasPermission
participant MenuItem
NavigationNavbarItem->>useHasPermission: resolve permissionKey
useHasPermission-->>NavigationNavbarItem: permitted or denied
NavigationNavbarItem->>MenuItem: pass permission metadata and handlers
MenuItem-->>NavigationNavbarItem: click or expansion event
NavigationNavbarItem-->>NavigationNavbarItem: execute or suppress interaction
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/custom/NavigationNavbar/navigationNavbar.tsx (1)
75-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWithhold denied-item handlers instead of assigning
noop.When
isPermittedis false,MenuItemand the expand icons still receive click callbacks. OmitonClickfor denied items and toggles.
src/custom/NavigationNavbar/navigationNavbar.tsx#L75-L76,L96,L127,L137-L139: Replace denied-pathnoopcallbacks with omitted orundefinedhandlers. Removenoopwhen it has no remaining use.AGENTS.md#L156-L159: Keep this statement only after the implementation actually withholds both handlers.Proposed fix
-const noop = () => {}; - - onClick={isPermitted ? item.onClick : noop} + onClick={isPermitted ? item.onClick : undefined} - onClick={isPermitted ? item.onClick : noop} + onClick={isPermitted ? item.onClick : undefined} - <ExpandLessIcon onClick={isPermitted ? (e) => onToggle(item.id, e) : noop} /> + <ExpandLessIcon onClick={isPermitted ? (e) => onToggle(item.id, e) : undefined} /> - <ExpandMoreIcon onClick={isPermitted ? (e) => onToggle(item.id, e) : noop} /> + <ExpandMoreIcon onClick={isPermitted ? (e) => onToggle(item.id, e) : undefined} />As per coding guidelines, “For navigation items that are not permitted, withhold both
onClickand the expand toggle in JavaScript.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/custom/NavigationNavbar/navigationNavbar.tsx` around lines 75 - 76, In navigationNavbar.tsx, update the denied-item paths in MenuItem and the expand-toggle handlers to pass omitted or undefined onClick values when isPermitted is false, then remove the unused noop helper; in AGENTS.md, retain the existing guidance only once both handlers are withheld in the implementation.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/__testing__/permissionKeySet.test.tsx`:
- Line 1: Update the Key import in permissionKeySet.test.tsx to be type-only,
since it is used solely for TypeScript annotations. Preserve the existing module
source and import usage while preventing runtime resolution of the optional peer
dependency.
In `@src/custom/PermissionProvider.tsx`:
- Around line 43-45: Replace the broad object check in isKeyList with runtime
validation of every required Key field, and centralize parsing for
getPermissionKeys, resolvePermissionKeySet, and getPermissionKeyCombinator.
Require a non-empty key array, exactly one defined combinator property, and
reject malformed members before userHasPermission; add regression coverage for {
anyOf: [{} as Key] } and { anyOf: [VIEW_ORG], allOf: undefined }.
In `@src/custom/permissions.tsx`:
- Around line 152-153: Update the metadata label preparation in the permissions
rendering flow: combine the category and subcategory values from displayedKeys
into one list, call uniqueDefined once on that combined list, and use the
resulting deduplicated labels where the chips are rendered instead of
concatenating separately deduplicated categories and subcategories.
- Around line 193-213: Update the clickable Box containing KeyIcon in the
key-copy action to use semantic button or icon-button behavior with a focusable
target and an appropriate aria-label. Preserve the existing copy logic and
ensure the click handler continues to call stopPropagation; add keyboard
activation through the chosen button semantics so keyboard users can copy the
key ID.
- Around line 202-210: Replace the raw hex and RGBA color literals in the `sx`
blocks for the copied key row and the related ranges with Sistent theme exports
or semantic palette tokens. Update both the default `color` values and `&:hover`
colors while preserving the existing copied-state and hover behavior across the
affected key rows and chips.
---
Nitpick comments:
In `@src/custom/NavigationNavbar/navigationNavbar.tsx`:
- Around line 75-76: In navigationNavbar.tsx, update the denied-item paths in
MenuItem and the expand-toggle handlers to pass omitted or undefined onClick
values when isPermitted is false, then remove the unused noop helper; in
AGENTS.md, retain the existing guidance only once both handlers are withheld in
the implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 811bf8c8-3017-4c72-ab26-3f94c7f26ef8
📒 Files selected for processing (11)
AGENTS.mdsrc/__testing__/permissionKeySet.test.tsxsrc/base/Button/Button.tsxsrc/base/IconButton/IconButton.tsxsrc/base/ListItem/ListItem.tsxsrc/base/ListItemButton/ListItemButton.tsxsrc/base/MenuItem/MenuItem.tsxsrc/custom/NavigationNavbar/navigationNavbar.tsxsrc/custom/PermissionProvider.tsxsrc/custom/permissions.tsxsrc/index.tsx
| @@ -0,0 +1,277 @@ | |||
| import { Key } from '@meshery/schemas/permissions'; | |||
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 2 \
-g 'package.json' \
-g 'tsconfig*.json' \
'"`@meshery/schemas`"|verbatimModuleSyntax|importsNotUsedAsValues|preserveValueImports' . || trueRepository: layer5io/sistent
Length of output: 643
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== permission test file =="
wc -l src/__testing__/permissionKeySet.test.tsx
sed -n '1,180p' src/__testing__/permissionKeySet.test.tsx
echo
echo "== package.json relevant sections =="
sed -n '1,180p' package.json
echo
echo "== tsconfig files =="
fd -a 'tsconfig.*\.json$' . | sed 's#^\./##' | while read -r f; do
echo "--- $f"
sed -n '1,220p' "$f"
done
echo
echo "== Key usages across repo =="
rg -n "import [^{]*\{[^}]*Key|import type.*Key|from ['\"]`@meshery/schemas/permissions`['\"]" .Repository: layer5io/sistent
Length of output: 14402
Make the Key import type-only.
Key is only used for TypeScript annotations in this test file. Use import type { Key } from '@meshery/schemas/permissions' so import-preserving builds do not resolve the optional peer dependency at runtime.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/__testing__/permissionKeySet.test.tsx` at line 1, Update the Key import
in permissionKeySet.test.tsx to be type-only, since it is used solely for
TypeScript annotations. Preserve the existing module source and import usage
while preventing runtime resolution of the optional peer dependency.
Source: Coding guidelines
| /** A non-empty array of key-shaped values. */ | ||
| const isKeyList = (value: unknown): value is Key[] => | ||
| Array.isArray(value) && value.length > 0 && value.every((k) => !!k && typeof k === 'object'); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject malformed permission key sets before host evaluation.
isKeyList accepts any non-null object as a Key, so malformed members can be passed to userHasPermission. Also, the current parser treats { anyOf: [...], allOf: undefined } as a valid anyOf because it checks undefined instead of requiring exactly one combinator property. Use one parser for getPermissionKeys, resolvePermissionKeySet, and getPermissionKeyCombinator that requires a non-empty key array, one combinator property, and valid runtime Key fields; add regression cases for { anyOf: [{} as Key] } and { anyOf: [VIEW_ORG], allOf: undefined }.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/custom/PermissionProvider.tsx` around lines 43 - 45, Replace the broad
object check in isKeyList with runtime validation of every required Key field,
and centralize parsing for getPermissionKeys, resolvePermissionKeySet, and
getPermissionKeyCombinator. Require a non-empty key array, exactly one defined
combinator property, and reject malformed members before userHasPermission; add
regression coverage for { anyOf: [{} as Key] } and { anyOf: [VIEW_ORG], allOf:
undefined }.
| const categories = uniqueDefined(displayedKeys.map((key) => key.category)); | ||
| const subcategories = uniqueDefined(displayedKeys.map((key) => key.subcategory)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Deduplicate metadata labels after combining them.
Lines 152-153 deduplicate categories and subcategories separately. Line 259 concatenates both arrays, so the same label appears twice when it occurs in both fields.
Build one combined value list, then call uniqueDefined once before rendering the chips.
Proposed fix
- const categories = uniqueDefined(displayedKeys.map((key) => key.category));
- const subcategories = uniqueDefined(displayedKeys.map((key) => key.subcategory));
+ const metadataLabels = uniqueDefined([
+ ...displayedKeys.map((key) => key.category),
+ ...displayedKeys.map((key) => key.subcategory)
+ ]);
...
- {[...categories, ...subcategories].map((label, index) => (
+ {metadataLabels.map((label, index) => (Also applies to: 259-271
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/custom/permissions.tsx` around lines 152 - 153, Update the metadata label
preparation in the permissions rendering flow: combine the category and
subcategory values from displayedKeys into one list, call uniqueDefined once on
that combined list, and use the resulting deduplicated labels where the chips
are rendered instead of concatenating separately deduplicated categories and
subcategories.
| <Tooltip title={copied ? 'Copied!' : 'Copy key ID to clipboard'} placement="top"> | ||
| <Box | ||
| component="span" | ||
| onClick={(e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| navigator.clipboard.writeText(key.id || ''); | ||
| setCopiedKeyId(key.id || `#${index}`); | ||
| setTimeout(() => setCopiedKeyId(null), 1500); | ||
| }} | ||
| sx={{ | ||
| display: 'inline-flex', | ||
| cursor: 'pointer', | ||
| color: copied ? '#EBC024' : 'rgba(255, 255, 255, 0.7)', | ||
| transition: 'color 0.2s ease', | ||
| '&:hover': { | ||
| color: '#EBC024' | ||
| } | ||
| }} | ||
| > | ||
| <KeyIcon sx={{ fontSize: '1rem' }} /> | ||
| </Box> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make each key-copy action keyboard-operable.
The new Box at Line 194 is a clickable span. It has no button semantics, focus target, accessible name, or keyboard handler. Keyboard-only users cannot copy a permission key ID.
Use a semantic button or an accessible icon button with an aria-label. Keep the click propagation stop in the new handler.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/custom/permissions.tsx` around lines 193 - 213, Update the clickable Box
containing KeyIcon in the key-copy action to use semantic button or icon-button
behavior with a focusable target and an appropriate aria-label. Preserve the
existing copy logic and ensure the click handler continues to call
stopPropagation; add keyboard activation through the chosen button semantics so
keyboard users can copy the key ID.
| sx={{ | ||
| display: 'inline-flex', | ||
| cursor: 'pointer', | ||
| color: copied ? '#EBC024' : 'rgba(255, 255, 255, 0.7)', | ||
| transition: 'color 0.2s ease', | ||
| '&:hover': { | ||
| color: '#EBC024' | ||
| } | ||
| }} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace new raw colors with Sistent semantic tokens.
These new key rows and chips use literal hex and RGBA colors. The values will not follow the active Sistent theme. Use Sistent theme exports and semantic palette tokens in these sx values.
As per coding guidelines, “Theme-aware UI must use Sistent theme exports and semantic palette tokens rather than raw MUI defaults.”
Also applies to: 215-238, 264-269
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/custom/permissions.tsx` around lines 202 - 210, Replace the raw hex and
RGBA color literals in the `sx` blocks for the copied key row and the related
ranges with Sistent theme exports or semantic palette tokens. Update both the
default `color` values and `&:hover` colors while preserving the existing
copied-state and hover behavior across the affected key rows and chips.
Source: Coding guidelines
Intent
Fix #1747:
permissionKeycannot express an OR of keys, so any affordance gated on more than one permission key cannot adoptPermissionShieldat all. The issue contains a fully specified, backwards-compatible design that was approved; implement that design.What to build: extend
permissionKeyto accept a key set with an explicit combinator, defaulting to today's single-key behaviour:permissionKey?: Key | { anyOf: Key[] } | { allOf: Key[] }.useHasPermissionresolvesanyOfwith.some(...)andallOfwith.every(...). A bareKeymust keep working byte-for-byte as it does today.PermissionShieldlists EVERY unmet key in its tooltip, not just one. ForanyOfthat means "needs any of: A, B"; forallOf, "needs all of: A, B". This is the whole point of the issue - the user currently gets no explanation at all.NavigationNavbarpreviously dropped the legacypermissionboolean as soon as a key was present (disabled={hasKey ? undefined : !permission}). A key SET must flow through that same path correctly and still render the shield.Section/parent items - required, and the reason this task exists: the downstream consumer needs this to work on a parent/section nav item, not only leaves. A section is reachable exactly when the user can reach at least one of its children, which is
{ anyOf: [...every child key...] }. Two things must hold for a section whose keys are all unmet: it renders aPermissionShieldexplaining why, and it cannot be expanded or clicked at all. IfNavigationNavbarlet a disabled section still toggle its children open, fixing that is part of this task. The exact complaint is that clicking such a section dumps the user on a "you don't have permission" error page.Acceptance criteria:
permissionKeyacceptsKey,{ anyOf: Key[] }, and{ allOf: Key[] }; types exported.Keybehaviour is unchanged - proven with a regression test. This library has many consumers; a silent narrowing of access here is the worst possible outcome.anyOfhit/miss,allOfhit/miss, and the shield tooltip listing all unmet keys.Context: this blocks layer5io/meshery-cloud, whose Identity -> Organizations nav item is gated on
ViewAllOrganizations || ViewOrgand is the one entry stuck on the legacy boolean path, so it shows as disabled with no explanation. Once this lands and is released, meshery-cloud bumps the sistent dependency and adopts key sets for both that item and the Identity section header. meshery-cloud is NOT changed here - that is a separate task.Keep the change proportional to the defect: a focused API extension plus its tests, not a permissions framework. Do not refactor unrelated permission code.
Decisions and tradeoffs made while implementing, which a reviewer reading only the diff would not know:
{anyOf, allOf}both present) set resolves tofalse. Rationale, stated in a code comment: supplying a set is an explicit statement that the affordance IS gated, so a set that cannot be evaluated must not fall through to "permitted"; and[].every(...)is vacuously true, so{ allOf: [] }would otherwise silently grant access to everyone. This is deliberate and covered by a five-case parameterised test.true, INCLUDING for a malformed set. This was a deliberate choice, not an oversight:PermissionProvider's documented contract is "if no PermissionProvider is present, all permission checks default to permitted - ensuring full backward compatibility". With no provider mounted the host has not wired up authorization at all, so nothing is evaluated and nothing is gated; the empty/malformed denial applies wherever permissions are actually evaluated. Both branches are tested.NavigationNavbarwas restructured into per-item components (NavigationNavbarItem/NavigationNavbarSubItem) because evaluating each item's permission requires a hook, and hooks cannot be called inside a.mapcallback. This is required by the fix, not gratuitous refactoring.onClick, withholds the expand-toggle handler, and forces the section collapsed (expanded = isPermitted && isOpen) - rather than only being styled disabled. This is the actual root cause: MUI enforcesdisabledon a non-<button>element (aMenuItemrenders<li>) purely withpointer-events: nonein theMui-disabledclass, andPermissionShieldblocks its children the same way, so the handlers stay attached and a "disabled" section could still be toggled open and activated. This also means legacypermission: falsenav items no longer fireonClickon the programmatic path; in a real browser that is unobservable because MUI's CSS already blocked the click, and it is covered by its own test. This is intentional and is the defect being fixed.PermissionShieldnow derives the unmet keys itself via a newuseUnmetPermissionKeyshook reading the provider context. It remains a pure visual component in the sense that it never second-guesses the caller's disabled decision: if the caller shields a key the user does in fact hold, it falls back to rendering the declared keys, which keeps the single-key rendering identical to today. A set that names no keys falls back to the original "Missing requisite key" wording rather than emitting a dangling "Needs any of: ".PermissionKeySet,PermissionKeySpec,isPermissionKeySet,useUnmetPermissionKeys) were added explicitly tosrc/index.tsx, per the repo's documentedrollup-plugin-dtsnested-barrel drop rule, and their presence in the builtdist/index.d.tswas verified.disabled/jsdom sharp edge, because a future agent testing disabled state would otherwise write a test that proves nothing.Verified locally: 22 new tests in
src/__testing__/permissionKeySet.test.tsx, full suite 492 passing across 27 suites,npm run lintclean,npm run buildclean. Pre-existing repo state that is NOT caused by this change:prettier --checkfails on many files andtsc --noEmitreports errors acrosssrc/- neither is a CI gate (see AGENTS.md); files already unformatted before this change were deliberately left unformatted rather than mass-reformatted.What Changed
permissionKey(on Button, IconButton, MenuItem, ListItem, ListItemButton,PermissionShield, anduseHasPermission) now acceptsKey | { anyOf: Key[] } | { allOf: Key[] }, resolved with.some(...)/.every(...). A bareKeybehaves exactly as before; an empty, non-array, nullish-member, or double-combinator set denies rather than falling through to permitted (so{ allOf: [] }cannot vacuously grant access), while the no-provider default stays unconditionally permitted.PermissionKeySet,PermissionKeySpec,isPermissionKeySet, anduseUnmetPermissionKeysare re-exported explicitly fromsrc/index.tsx.PermissionShieldderives the unmet keys from provider context via the newuseUnmetPermissionKeyshook and lists all of them in its tooltip —Needs any of: A, BforanyOf,Needs all of: A, BforallOf— with category/subcategory chips deduped across displayed keys. It never overrides the caller's disabled decision, and a set that resolves to no keys falls back to the original "Missing requisite key" wording.NavigationNavbarwas split into per-itemNavigationNavbarItem/NavigationNavbarSubItemcomponents so each item can evaluate its own permission hook (hooks can't run inside a.mapcallback). An unpermitted item is now genuinely inert in JS —onClickand the expand toggle are withheld and the section is forced collapsed (expanded = isPermitted && isOpen) — instead of relying on MUI'spointer-events: nonestyling, so a shielded section can no longer be opened or activated. Covered by 22 new tests insrc/__testing__/permissionKeySet.test.tsx(the two nav-inertness tests were confirmed to fail against the pre-fix navbar), plus an AGENTS.md note recording the MUI-disabled/jsdom sharp edge.Risk Assessment
✅ Low: A well-bounded, backwards-compatible widening of an approved API: the bare-Key path is provably unchanged, the deny-on-malformed semantics and the nav-inertness fix are explicitly decided and covered by tests, new exports follow the repo's root re-export rule, and only informational nits remain — none blocking.
Testing
Ran the focused suite for this change (
permissionKeySet.test.tsx, 22 tests) plus the adjacent permission, nav-type and dist-backed export guards — all green — and confirmed the new tests are not vacuous by replaying the two nav-inertness cases against the pre-fix navbar, where exactly those two fail. Verified the API is actually published by building and findingPermissionKeySet,PermissionKeySpec,isPermissionKeySetanduseUnmetPermissionKeysindist/index.d.tsand the runtime bundle. For end-user evidence I bundled the library fromsrc/and drove it in real headless Chromium reproducing the meshery-cloud Identity/Organizations case with real generated permission keys, capturing eight screenshots, a screen recording and a click-by-click transcript: the tooltip now names every unmet key for both combinators, bare-Keywording is byte-identical to before, a malformed{ allOf: [] }denies, and an unreachable section resists both real pointer clicks and programmatic click dispatches that bypass MUI'spointer-events: none, while satisfying one arm of theanyOfrestores expansion and navigation. Removed thedist/build output afterwards; the working tree is clean./tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/03-anyOf-tooltip-lists-every-unmet-key.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/02-no-permissions-section-cannot-expand.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/07-anyOf-satisfied-navigates.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/04-allOf-tooltip.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/05-bare-key-tooltip-unchanged.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/01-no-permissions-overview.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/06-anyOf-satisfied-section-expands.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/08-admin-profile-allOf-unlocked.png)/tmp/no-mistakes-evidence/01KZ5MGH8XCB94Z0TMMXHBJT4M/09-walkthrough.webm)Evidence: Browser transcript — every click and the exact tooltip text read from the live DOM
anyOf section tooltip : Authorization Required | Needs any of: View All Organizations, View Organizations | View All Organizations — See all organizations within a Cloud deployment... | View Organizations — See all organizations of which you are an administrator... allOf button tooltip : Needs all of: View Organizations, Create Organization bare-Key tooltip : Missing requisite key | Create Organization empty-set tooltip : Missing requisite key (i.e. { allOf: [] } denies, no dangling combinator text) real pointer click at the chevron -> intercepted by the shield overlay; child items rendered: 0 programmatic click, chevron -> child items rendered: 0 programmatic click, section row -> last navigation event: "—" [profile holding only View Organizations] real pointer click on chevron -> child items rendered: 1 real pointer click, Organizations -> last navigation event: "NAVIGATED: Identity -> Organizations clicked" real pointer click, Identity row -> last navigation event: "NAVIGATED: Identity section clicked" Teams (bare Key, still unheld) -> shielded + disabledEvidence: Pre-fix non-vacuity check for the two nav-inertness tests
Evidence: New public exports present in the built declaration + runtime bundles
Evidence: Evidence index + reproduction harness
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
src/custom/permissions.tsx:198- The tooltip now renders one copy button per unmet key but still shares a singlecopiedKeyIdplus an unconditionalsetTimeout(() => setCopiedKeyId(null), 1500). Each click schedules its own timer that clears whatever is current, so a second copy is cut short by the first key's timer. Fix:setCopiedKeyId((cur) => (cur === thisId ? null : cur))in the timeout. Newly reachable only because a set can now display multiple keys.src/custom/PermissionProvider.tsx:50-getPermissionKeys(line 50),resolvePermissionKeySet(line 67) andgetPermissionKeyCombinator(line 80) each independently re-cast the spec to{ anyOf?: unknown; allOf?: unknown }, re-apply the double-combinator rule and (in two of three) re-applyisKeyList. They agree today, but the resolver decides whether access is granted while the other two decide what the shield says, so any future edit to one rule silently desynchronises the gate from its explanation. A singlenormalizeKeySpec(spec) -> { combinator, keys }that all three consume would make that drift impossible; both helpers are module-internal to the public API (neither is re-exported from src/index.tsx), so the refactor is contained.src/custom/PermissionProvider.tsx:45-isKeyListrequires every member to be a truthy object, so one bad member denies the whole set — includinganyOf. Concretely:{ anyOf: [Keys.ViewAllOrganizations, Keys.ViewOrg] }where one constant resolved toundefinedbecause meshery/schemas renamed it (AGENTS.md records that a patch release renamed 10 key constants with UUIDs unchanged) denies the section outright, even for a user who holds the surviving key, and the shield then shows the bare 'Missing requisite key' wording with no keys listed. This is the explicitly decided fail-closed semantic and is tested, so no change is requested — noting it because the downstream adopter (meshery-cloud) will hit it as a silent access loss, not a build error, on a schemas bump.src/custom/permissions.tsx:85- The component docstring still asserts "This is a pure visual component — it does NOT check permissions itself." It now callsuseUnmetPermissionKeys(line 100) and reads the provider context to choose which keys to list. The inline comment at lines 136-139 states the refined contract correctly (it never overrides the caller's disabled decision, only picks what to display); the header comment above it should say the same so consumers reading the IDE tooltip aren't told the component ignores the provider.✅ **Test** - passed
✅ No issues found.
npx jest src/__testing__/permissionKeySet.test.tsx— 22 tests, all passingnpx jest src/__testing__/permissions.test.ts src/__testing__/navigationItemTitleTypes.test.ts— adjacent permission/nav guardsNon-vacuity check:git checkout a57f6b42 -- src/custom/NavigationNavbar/navigationNavbar.tsxthennpx jest src/__testing__/permissionKeySet.test.tsx -t "NavigationNavbar"— exactly the 2 inertness tests fail pre-fix; navbar restored and tree left cleannpm run buildthengrep -nE "PermissionKeySet|PermissionKeySpec|isPermissionKeySet|useUnmetPermissionKeys" dist/index.d.ts— all four new symbols in the declaration bundle and root export listnode -e "require('./dist/index.js')"export check —isPermissionKeySet/useUnmetPermissionKeyspresent at runtimenpx jest src/__testing__/publishedTypeSurfaceDependencies.test.ts src/__testing__/optionalPeerDependencies.test.ts— dist-backed public-surface guards against the newly added exportsManual browser verification: esbuild-bundled demo ofsrc/driven by Playwright in headless Chromium (demo/build.mjs,demo/shots.mjs) — Identity section + Organizations item gated on{ anyOf: [ViewAllOrganizations, ViewOrganizations] }using real@meshery/schemas/permissionskeys, across three permission profilesBrowser: opened theanyOfsection shield tooltip and read live DOM text —Needs any of: View All Organizations, View Organizationsplus both key descriptionsBrowser:allOftooltip readsNeeds all of: …; bareKeytooltip still readsMissing requisite key;{ allOf: [] }denies and falls back to the original wording with no dangling combinator textBrowser: unreachable section attacked with a real pointer click on the expand chevron and two programmaticMouseEvent('click')dispatches at the chevron and the row (bypassingpointer-events: none) — 0 child items rendered, no navigation firedBrowser: profile holding onlyView Organizationsunshields the section, expands it on a real click and navigates; theTeamschild on a bare unheldKeystays shielded and disabledfind src -name "*.stories.*" -o -name "*.mdx"— confirms DangerConfirmationModal is the only stories file, so no Storybook surface documents these propsAGENTS.md:108- Out-of-scope consolidation worth a follow-up: the paragraph explaining thatKeyis the permission-key contract behindpermissionKeyon Button/IconButton/MenuItem/ListItem/ListItemButton,PermissionShield,PermissionProvideranduseHasPermission, and that a silent collapse toanyun-checks those props, exists in full in two places — AGENTS.md:108-110 and the header comment of src/testing/publishedTypeSurfaceDependencies.test.ts:16-21. The duplication predates this change and both copies are still accurate (PermissionKeySpec is built fromKey, so the collapse consequence is unchanged), so nothing here is stale. Not touched under scope discipline; the follow-up would reduce the AGENTS.md copy to a pointer at the guard test, which the placement policy names as the owner of that detail.src/custom/permissions.tsx:321-prettier --checkstill fails on three files this change touched (src/custom/permissions.tsx, src/custom/PermissionProvider.tsx, src/base/MenuItem/MenuItem.tsx). Each deviation was verified to be present at base commit a57f6b4 by running prettier over the base blobs, so none was introduced here: an unwrapped<Box sx={...}>/<UsersIcon>block in the tooltip user-context section, two double blank lines in PermissionProvider.tsx, and organize-imports wanting to deleteimport React from 'react'in MenuItem.tsx. Left unfixed deliberately: prettier is not a CI gate (AGENTS.md "Repo state that looks broken but is pre-existing"), fixing them would mass-reformat regions this change never touched, and the MenuItem React-import removal is not a safe mechanical edit. The one prettier deviation this change did introduce — named-import ordering in permissions.tsx — was fixed. eslint, the actual gate, is clean.✅ **Push** - passed
✅ No issues found.
Summary by CodeRabbit
anyOf/allOfkey sets.