diff --git a/package-lock.json b/package-lock.json
index d4565b070..843484d75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"@emotion/cache": "^11.14.0",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
- "@meshery/schemas": "1.3.13",
+ "@meshery/schemas": "^1.3.25",
"@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0",
"@mui/system": "^9.0.0",
@@ -3355,9 +3355,9 @@
}
},
"node_modules/@meshery/schemas": {
- "version": "1.3.13",
- "resolved": "https://registry.npmjs.org/@meshery/schemas/-/schemas-1.3.13.tgz",
- "integrity": "sha512-VZw8yrKiBrxvJxFysOMQuuPJkDwGZ2DuW7ifPGJ6OxfBygAGIYuN4kofmPTglcW/WPhEYn656HM7/zahSmlcRg==",
+ "version": "1.3.25",
+ "resolved": "https://registry.npmjs.org/@meshery/schemas/-/schemas-1.3.25.tgz",
+ "integrity": "sha512-wsFu2nkIKUtVBLekbDyZ0lesEMOaxEBlyYeqRSa0Y02oMAZ+ubfpnTDPiDEOUvPYxCBQfWx1vYUsbqRaU5odWA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
diff --git a/package.json b/package.json
index 7626f3275..253efe9f8 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"@emotion/cache": "^11.14.0",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^10.0.1",
- "@meshery/schemas": "1.3.13",
+ "@meshery/schemas": "^1.3.25",
"@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0",
"@mui/system": "^9.0.0",
diff --git a/src/base/Button/Button.tsx b/src/base/Button/Button.tsx
index 9eefa74a1..d03051eb7 100644
--- a/src/base/Button/Button.tsx
+++ b/src/base/Button/Button.tsx
@@ -1,37 +1,56 @@
import { Button as MuiButton, type ButtonProps as MuiButtonProps } from '@mui/material';
import React from 'react';
+import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider';
import { Key, PermissionShield } from '../../custom/permissions';
export interface ButtonProps extends MuiButtonProps {
label?: string;
children?: React.ReactNode;
permissionKey?: Key;
+ /**
+ * Determines behavior when the user lacks the required permission.
+ *
+ * - `'showShield'` (default) — disables the button and shows a shield icon
+ * with a permission-metadata tooltip.
+ * - `'hide'` — renders nothing.
+ *
+ * Ignored when `permissionKey` is not provided.
+ */
+ permissionAction?: PermissionAction;
}
export function Button({
label,
children,
permissionKey,
+ permissionAction = 'showShield',
disabled,
...props
}: ButtonProps): JSX.Element {
- // When disabled AND permissionKey is provided, show the shield overlay
- if (disabled && permissionKey) {
+ const hasPermission = useHasPermission(permissionKey);
+
+ // useHasPermission returns true when no permissionKey is provided (backward compatible)
+ if (hasPermission) {
return (
-
-
- {label}
- {children}
-
-
+
+ {label}
+ {children}
+
);
}
+ // User LACKS permission → apply the permissionAction
+ if (permissionAction === 'hide') {
+ return <>>;
+ }
+
return (
-
- {label}
- {children}
-
+
+
+ {label}
+ {children}
+
+
);
}
diff --git a/src/base/IconButton/IconButton.tsx b/src/base/IconButton/IconButton.tsx
index 778158ee2..98605dd90 100644
--- a/src/base/IconButton/IconButton.tsx
+++ b/src/base/IconButton/IconButton.tsx
@@ -3,25 +3,42 @@ import {
type IconButtonProps as MuiIconButtonProps
} from '@mui/material';
import React from 'react';
+import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider';
import { Key, PermissionShield } from '../../custom/permissions';
export interface IconButtonProps extends MuiIconButtonProps {
permissionKey?: Key;
+ /**
+ * Determines behavior when the user lacks the required permission.
+ *
+ * - `'showShield'` (default) — disables the button and shows a shield icon
+ * with a permission-metadata tooltip.
+ * - `'hide'` — renders nothing.
+ *
+ * Ignored when `permissionKey` is not provided.
+ */
+ permissionAction?: PermissionAction;
}
export const IconButton = React.forwardRef((props, ref) => {
- const { permissionKey, disabled, ...rest } = props;
+ const { permissionKey, permissionAction = 'showShield', disabled, ...rest } = props;
+ const hasPermission = useHasPermission(permissionKey);
- // When disabled AND permissionKey is provided, show the shield overlay
- if (disabled && permissionKey) {
- return (
-
-
-
- );
+ // useHasPermission returns true when no permissionKey is provided (backward compatible)
+ if (hasPermission) {
+ return ;
}
- return ;
+ // User LACKS permission → apply the permissionAction
+ if (permissionAction === 'hide') {
+ return null;
+ }
+
+ return (
+
+
+
+ );
});
IconButton.displayName = 'IconButton';
diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx
index eb3ec7981..af6abbffc 100644
--- a/src/base/ListItem/ListItem.tsx
+++ b/src/base/ListItem/ListItem.tsx
@@ -1,27 +1,43 @@
import { ListItem as MuiListItem, ListItemProps as MuiListItemProps } from '@mui/material';
import React from 'react';
+import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider';
import { Key, PermissionShield } from '../../custom/permissions';
export interface ListItemProps extends MuiListItemProps {
permissionKey?: Key;
disabled?: boolean;
+ /**
+ * Determines behavior when the user lacks the required permission.
+ *
+ * - `'showShield'` (default) — disables the item and shows a shield icon
+ * with a permission-metadata tooltip.
+ * - `'hide'` — renders nothing.
+ *
+ * Ignored when `permissionKey` is not provided.
+ */
+ permissionAction?: PermissionAction;
}
const ListItem = React.forwardRef((props, ref) => {
- const { permissionKey, disabled, ...rest } = props;
+ const { permissionKey, permissionAction = 'showShield', ...rest } = props;
+ delete (rest as Record).disabled;
+ const hasPermission = useHasPermission(permissionKey);
- // When disabled AND permissionKey is provided, show the shield overlay
- // Note: MUI ListItem doesn't have a native `disabled` prop, so we only use
- // it as a custom guard for the PermissionShield wrapper
- if (disabled && permissionKey) {
- return (
-
-
-
- );
+ // useHasPermission returns true when no permissionKey is provided (backward compatible)
+ if (hasPermission) {
+ return ;
}
- return ;
+ // User LACKS permission → apply the permissionAction
+ if (permissionAction === 'hide') {
+ return null;
+ }
+
+ return (
+
+
+
+ );
});
ListItem.displayName = 'ListItem';
diff --git a/src/base/ListItemButton/ListItemButton.tsx b/src/base/ListItemButton/ListItemButton.tsx
index d5125aa76..84b30819e 100644
--- a/src/base/ListItemButton/ListItemButton.tsx
+++ b/src/base/ListItemButton/ListItemButton.tsx
@@ -3,25 +3,42 @@ import {
ListItemButtonProps as MuiListItemButtonProps
} from '@mui/material';
import React from 'react';
+import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider';
import { Key, PermissionShield } from '../../custom/permissions';
export interface ListItemButtonProps extends MuiListItemButtonProps {
permissionKey?: Key;
+ /**
+ * Determines behavior when the user lacks the required permission.
+ *
+ * - `'showShield'` (default) — disables the button and shows a shield icon
+ * with a permission-metadata tooltip.
+ * - `'hide'` — renders nothing.
+ *
+ * Ignored when `permissionKey` is not provided.
+ */
+ permissionAction?: PermissionAction;
}
const ListItemButton = React.forwardRef((props, ref) => {
- const { permissionKey, disabled, ...rest } = props;
+ const { permissionKey, permissionAction = 'showShield', disabled, ...rest } = props;
+ const hasPermission = useHasPermission(permissionKey);
- // When disabled AND permissionKey is provided, show the shield overlay
- if (disabled && permissionKey) {
- return (
-
-
-
- );
+ // useHasPermission returns true when no permissionKey is provided (backward compatible)
+ if (hasPermission) {
+ return ;
}
- return ;
+ // User LACKS permission → apply the permissionAction
+ if (permissionAction === 'hide') {
+ return null;
+ }
+
+ return (
+
+
+
+ );
});
ListItemButton.displayName = 'ListItemButton';
diff --git a/src/base/MenuItem/MenuItem.tsx b/src/base/MenuItem/MenuItem.tsx
index d46d2ee7a..e626ab1c1 100644
--- a/src/base/MenuItem/MenuItem.tsx
+++ b/src/base/MenuItem/MenuItem.tsx
@@ -1,24 +1,41 @@
import { MenuItem as MuiMenuItem, MenuItemProps as MuiMenuItemProps } from '@mui/material';
import React from 'react';
+import { useHasPermission, type PermissionAction } from '../../custom/PermissionProvider';
import { Key, PermissionShield } from '../../custom/permissions';
export interface MenuItemProps extends MuiMenuItemProps {
permissionKey?: Key;
+ /**
+ * Determines behavior when the user lacks the required permission.
+ *
+ * - `'showShield'` (default) — disables the item and shows a shield icon
+ * with a permission-metadata tooltip.
+ * - `'hide'` — renders nothing.
+ *
+ * Ignored when `permissionKey` is not provided.
+ */
+ permissionAction?: PermissionAction;
}
export function MenuItem(props: MenuItemProps): JSX.Element {
- const { permissionKey, disabled, ...rest } = props;
+ const { permissionKey, permissionAction = 'showShield', disabled, ...rest } = props;
+ const hasPermission = useHasPermission(permissionKey);
- // When disabled AND permissionKey is provided, show the shield overlay
- if (disabled && permissionKey) {
- return (
-
-
-
- );
+ // useHasPermission returns true when no permissionKey is provided (backward compatible)
+ if (hasPermission) {
+ return ;
}
- return ;
+ // User LACKS permission → apply the permissionAction
+ if (permissionAction === 'hide') {
+ return <>>;
+ }
+
+ return (
+
+
+
+ );
}
export default MenuItem;
diff --git a/src/custom/PermissionProvider.tsx b/src/custom/PermissionProvider.tsx
new file mode 100644
index 000000000..d799de516
--- /dev/null
+++ b/src/custom/PermissionProvider.tsx
@@ -0,0 +1,127 @@
+import { Key } from '@meshery/schemas/permissions';
+import React, { createContext, useContext } from 'react';
+
+
+/**
+ * Determines how a component responds when the user lacks the required permission.
+ *
+ * - `'hide'` — renders nothing (`null`).
+ * - `'showShield'` — disables the component, overlays a shield/lock icon, and
+ * displays permission metadata in a tooltip.
+ */
+export type PermissionAction = 'hide' | 'showShield';
+
+/**
+ * User context displayed inside the PermissionShield tooltip.
+ *
+ * Passed through the provider so that `PermissionShield` never reads from
+ * `sessionStorage` or any other host-specific storage mechanism.
+ */
+export interface PermissionUserContext {
+ userName?: string;
+ orgName?: string;
+ roleNames?: string[];
+}
+
+/**
+ * Value exposed by `PermissionContext`.
+ */
+export interface PermissionProviderValue {
+ /**
+ * Generic permission evaluator supplied by the host application.
+ *
+ * Sistent never knows *how* permissions are checked (CASL, server lookup,
+ * JWT claim, etc.) — it only calls this function.
+ */
+ userHasPermission: (key: Key) => boolean;
+
+ /** Optional user context rendered inside the shield tooltip. */
+ userContext?: PermissionUserContext;
+}
+
+
+const PermissionContext = createContext(null);
+
+// Provider
+
+export interface PermissionProviderProps {
+ /**
+ * A function that returns `true` when the current user holds the given
+ * permission key. The host application is responsible for implementing
+ * this — it may delegate to CASL, a server-side API, a JWT claim, etc.
+ */
+ userHasPermission: (key: Key) => boolean;
+
+ /**
+ * Optional user/org/role context displayed inside the `PermissionShield`
+ * tooltip. When omitted the tooltip's user-context section is hidden.
+ */
+ userContext?: PermissionUserContext;
+
+ children: React.ReactNode;
+}
+
+/**
+ * `PermissionProvider` — the single integration point between Sistent's
+ * permission-aware components and the host application's authorization system.
+ *
+ * Mount this near the root of the React tree (alongside your theme provider).
+ * If no `PermissionProvider` is present, all permission checks default to
+ * "permitted" — ensuring full backward compatibility.
+ *
+ * @example
+ * ```tsx
+ * // In _app.tsx — CASL adapter (the ONLY place CASL is referenced)
+ * import { PermissionProvider } from '@sistent/sistent';
+ * import { ability } from '@/utils/can';
+ *
+ * const userHasPermission = (key) => ability.can(key.id, key.function?.toLowerCase());
+ *
+ *
+ * {children}
+ *
+ * ```
+ */
+export const PermissionProvider: React.FC = ({
+ userHasPermission,
+ userContext,
+ children
+}) => (
+
+ {children}
+
+);
+
+PermissionProvider.displayName = 'PermissionProvider';
+
+// Hooks
+
+/**
+ * Access the full `PermissionProviderValue`.
+ * Returns `null` when no `PermissionProvider` is mounted.
+ */
+export const usePermission = (): PermissionProviderValue | null => useContext(PermissionContext);
+
+/**
+ * Check whether the current user has a given permission key.
+ *
+ * Returns `true` when:
+ * - No `PermissionProvider` is mounted (backward-compatible default), OR
+ * - No `key` is supplied, OR
+ * - `userHasPermission(key)` returns `true`.
+ */
+export const useHasPermission = (key?: Key): boolean => {
+ const ctx = usePermission();
+ if (!key || !ctx) return true;
+ return ctx.userHasPermission(key);
+};
+
+/**
+ * Retrieve the user context supplied to `PermissionProvider`.
+ * Used internally by `PermissionShield` to display user/org/role info
+ * without reading from `sessionStorage`.
+ */
+export const usePermissionUserContext = (): PermissionUserContext | undefined => {
+ const ctx = usePermission();
+ return ctx?.userContext;
+};
diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx
index 463cbcbb2..5e085e4e8 100644
--- a/src/custom/permissions.tsx
+++ b/src/custom/permissions.tsx
@@ -1,9 +1,12 @@
+import { Key } from '@meshery/schemas/permissions';
+export type { Key };
import KeyIcon from '@mui/icons-material/Key';
import LaunchIcon from '@mui/icons-material/Launch';
import SecurityIcon from '@mui/icons-material/Security';
import React from 'react';
import { EventBus } from '../actors/eventBus';
import { Box, Chip, ClickAwayListener, Link, Tooltip, Typography } from '../base';
+import { usePermissionUserContext } from './PermissionProvider';
const DIVIDER_SX = {
height: '1px',
@@ -11,19 +14,6 @@ const DIVIDER_SX = {
my: 1.25
};
-export interface Key {
- // Backwards compatibility for old CanShow
- subject?: string;
- action?: string;
-
- // New Schemas key structure
- id?: string;
- category?: string;
- subcategory?: string;
- function?: string;
- description?: string;
-}
-
export type InvertAction = 'disable' | 'hide';
export type MissingPermissionReason = {
@@ -43,7 +33,7 @@ export type MissingCapabilityReason = {
export type ReasonEvent = MissingPermissionReason | MissingCapabilityReason;
export interface HasKeyProps {
- Key?: Key;
+ Key?: Key & { action?: string; subject?: string };
predicate?: (capabilitiesRegistry: unknown) => [boolean, ReasonEvent];
children: React.ReactNode;
notifyOnclick?: boolean;
@@ -74,6 +64,7 @@ export const PermissionShield: React.FC = ({
const [open, setOpen] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const uniqueId = React.useId();
+ const userContext = usePermissionUserContext();
const handleClose = () => {
setOpen(false);
@@ -147,7 +138,7 @@ export const PermissionShield: React.FC = ({
component="span"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
- navigator.clipboard.writeText(permissionKey.id || permissionKey.action || '');
+ navigator.clipboard.writeText(permissionKey.id || '');
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
@@ -172,7 +163,7 @@ export const PermissionShield: React.FC = ({
color: '#FFFFFF'
}}
>
- {permissionKey.function || permissionKey.subject || 'Access Restricted'}
+ {permissionKey.function || 'Access Restricted'}
@@ -187,7 +178,7 @@ export const PermissionShield: React.FC = ({
}}
>
{permissionKey.description ||
- `Allows you to perform the ${permissionKey.function || permissionKey.subject || 'selected'} operation.`}
+ `Allows you to perform the ${permissionKey.function || 'selected'} operation.`}
@@ -264,100 +255,84 @@ export const PermissionShield: React.FC = ({
- {/* User / Org / Role context — read from sessionStorage */}
- {(() => {
- try {
- const org = JSON.parse(sessionStorage.getItem('currentOrg') || 'null');
- const user = JSON.parse(sessionStorage.getItem('user') || 'null');
- const orgName = org?.name;
- const firstName = user?.firstName || user?.first_name || '';
- const lastName = user?.lastName || user?.last_name || '';
- const userName = `${firstName} ${lastName}`.trim() || user?.name || user?.email;
- const orgId = org?.id;
- const orgWithRoles = user?.organizations?.organizationsWithRoles?.find(
- (o: { id?: string; roleNames?: string[] }) => o.id === orgId
- );
- const roleNames: string[] = orgWithRoles?.roleNames || user?.roleNames || [];
-
- if (!orgName && !userName) return null;
-
- return (
- <>
-
-
+
+
+ {userContext.userName && (
+
+
+ User
+
+
+ {userContext.userName}
+
+
+ )}
+
+
+ Org
+
+
-
-
- User
-
-
- {userName}
-
-
-
-
- Org
-
-
- {orgName || 'Private Org'}
-
-
-
-
- Role(s)
-
-
- {roleNames.length > 0 ? roleNames.join(', ') : 'None'}
-
-
-
+ {userContext.orgName || 'Private Org'}
+
+
+
+
+ Role(s)
+
- Seeing this message in error? Contact your Admins to request access.
+ {userContext.roleNames && userContext.roleNames.length > 0
+ ? userContext.roleNames.join(', ')
+ : 'None'}
- >
- );
- } catch {
- return null;
- }
- })()}
+
+
+
+ Seeing this message in error? Contact your Admins to request access.
+
+ >
+ )}
);
@@ -539,3 +514,17 @@ export const createCanShow = (
);
};
};
+
+// Re-export PermissionProvider types and hooks
+export {
+ PermissionProvider,
+ usePermission,
+ useHasPermission,
+ usePermissionUserContext
+} from './PermissionProvider';
+export type {
+ PermissionAction,
+ PermissionProviderValue,
+ PermissionUserContext,
+ PermissionProviderProps
+} from './PermissionProvider';
diff --git a/src/index.tsx b/src/index.tsx
index 1129de512..5eff2e6ab 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -47,7 +47,15 @@ export {
} from './custom/UniversalFilter';
export {
+ PermissionProvider,
PermissionShield,
+ usePermission,
+ useHasPermission,
+ usePermissionUserContext,
type Key,
- type PermissionShieldProps
+ type PermissionAction,
+ type PermissionProviderProps,
+ type PermissionProviderValue,
+ type PermissionShieldProps,
+ type PermissionUserContext
} from './custom/permissions';