Admin-side permission UI registration and DI-backed permission checking. Use this skill when adding permission controls to the admin UI — schema-based auto-generated forms, injectable permissions via createPermissionsAbstraction/ createPermissionsFeature, typed hooks (createUsePermissions), the HasPermission component (createHasPermission), and the Security.Permissions component props. Covers both simple apps and complex multi-entity permission schemas.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Permissions follow three layers: domain (schema), features (DI artifacts + registration), and presentation (hooks, components, UI config). The framework auto-generates the permission UI from a schema and provides injectable permission checking via the DI container.
Define the schema in src/domain/permissionsSchema.ts:
import { createPermissionSchema } from "webiny/admin/security";
export const SM_PERMISSIONS_SCHEMA = createPermissionSchema({
prefix: "sm",
fullAccess: true,
entities: [
{
id: "product",
title: "Products",
permission: "sm.product",
scopes: ["full", "own"],
actions: [
{ name: "rwd" },
{ name: "pw" },
{ name: "import", label: "Import products" },
{ name: "export", label: "Export products" }
]
},
{
id: "category",
title: "Categories",
permission: "sm.category",
scopes: ["full"],
actions: [{ name: "rwd" }]
},
{
id: "settings",
title: "Settings",
permission: "sm.settings",
scopes: ["full"]
}
]
});| Field | Type | Required | Description |
|---|---|---|---|
prefix | string | Yes | Permission prefix (e.g., "sm") |
fullAccess | true | object | Yes | true for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., { canForceUnlock: true }). |
readOnlyAccess | boolean | No | Whether to show a "Read-only access" option |
entities | EntityDefinition[] | No | Entity definitions. Omit for binary full/no access. |
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier for form field naming |
title | string | No | Display title. Falls back to id. |
permission | string | Yes | Permission name emitted (e.g., "sm.product") |
scopes | ("full" | "own")[] | Yes | Available access scopes |
actions | ActionDefinition[] | No | Actions on this entity |
dependsOn | { entity: string; requires: string } | No | Dependency on another entity |
{ name: "rwd" } — Read/Write/Delete select dropdown. Auto-set to "rwd" when scope is "own".{ name: "pw" } — Publish/Unpublish checkbox group.{ name: "custom", label: "Label" } — Custom boolean flag.Child entities can depend on a parent. If the parent lacks the required action, the child is pruned from output. "own" scope cascades to dependents.
{
id: "review",
permission: "sm.review",
scopes: ["full", "own"],
actions: [{ name: "rwd" }],
dependsOn: { entity: "product", requires: "r" }
}Omit entities for binary full/no access:
export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
prefix: "ma",
fullAccess: true
});src/features/permissions/abstractions.ts)import { createPermissionsAbstraction } from "webiny/admin/security";
import type { Permissions } from "webiny/admin/security";
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
export const SmPermissions = createPermissionsAbstraction(SM_PERMISSIONS_SCHEMA);
export namespace SmPermissions {
export type Interface = Permissions<typeof SM_PERMISSIONS_SCHEMA>;
}src/features/permissions/feature.ts)import { createPermissionsFeature } from "webiny/admin/security";
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
import { SmPermissions } from "./abstractions.js";
export const SmPermissionsFeature = createPermissionsFeature(SM_PERMISSIONS_SCHEMA, SmPermissions);Register the feature and the permission UI in your extension component:
import { AdminConfig, RegisterFeature } from "webiny/admin/security";
import { ReactComponent as Icon } from "@webiny/icons/shield.svg";
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
import { SmPermissionsFeature } from "~/features/permissions/feature.js";
const { Security } = AdminConfig;
export const Extension = () => {
return (
<>
<RegisterFeature feature={SmPermissionsFeature} />
<AdminConfig>
<Security.Permissions
name="store-manager"
title="Store Manager"
description="Manage Store Manager permissions."
icon={<Icon />}
schema={SM_PERMISSIONS_SCHEMA}
/>
{/* Routes, menus, etc. */}
</AdminConfig>
</>
);
};usePermissions Hook// src/presentation/security/usePermissions.ts
import { createUsePermissions } from "webiny/admin/security";
import { SmPermissions } from "~/features/permissions/abstractions.js";
export const usePermissions = createUsePermissions(SmPermissions);Usage:
const permissions = usePermissions();
permissions.canAccess("product"); // has any access
permissions.canRead("product"); // rwd includes "r"
permissions.canCreate("product"); // rwd includes "w"
permissions.canEdit("product", item); // rwd includes "w", respects own scope
permissions.canDelete("product"); // rwd includes "d"
permissions.canPublish("product"); // pw includes "p"
permissions.canUnpublish("product"); // pw includes "u"
permissions.canAction("import", "product"); // custom boolean flagEntity IDs are fully typed — canRead("bogus") produces a type error.
HasPermission Component// src/presentation/security/HasPermission.tsx
import { createHasPermission } from "webiny/admin/security";
import { SmPermissions } from "~/features/permissions/abstractions.js";
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
export const HasPermission = createHasPermission(SmPermissions, SM_PERMISSIONS_SCHEMA);Usage in JSX:
<HasPermission entity="product">
{/* Rendered if user can access "product" */}
</HasPermission>
<HasPermission any={["product", "category"]}>
{/* Rendered if user can access ANY of these entities */}
</HasPermission>
<HasPermission all={["product", "category"]}>
{/* Rendered if user can access ALL of these entities */}
</HasPermission>
<HasPermission entity="product" action="read">
{/* Rendered if user canRead("product") */}
</HasPermission>
<HasPermission entity="product" allActions={["read", "publish"]}>
{/* Rendered if user canRead AND canPublish "product" */}
</HasPermission>
<HasPermission entity="product" someActions={["import", "export"]}>
{/* Rendered if user can do ANY of these custom actions */}
</HasPermission>Inject permissions into feature implementations:
import type { SmPermissions } from "~/features/permissions/abstractions.js";
import { SmPermissions as SmPermissionsAbstraction } from "~/features/permissions/abstractions.js";
class SomeFeatureImpl {
constructor(private permissions: SmPermissions.Interface) {}
doSomething() {
if (this.permissions.canEdit("product")) {
// ...
}
}
}
const SomeFeature = SomeAbstraction.createImplementation({
implementation: SomeFeatureImpl,
dependencies: [SmPermissionsAbstraction]
});Security.Permissions Props| Prop | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for this permission renderer |
title | string | Yes | Display title in the accordion header |
description | string | No | Description shown below the title |
icon | ReactElement | No | Icon in the accordion header |
schema | PermissionSchema | One of schema/element | Auto-generate UI from schema |
element | ReactElement | One of schema/element | Fully custom permission UI |
system | boolean | No | If true, renders before app-level permissions |
src/
├── domain/
│ └── permissionsSchema.ts # createPermissionSchema()
├── features/
│ └── permissions/
│ ├── abstractions.ts # createPermissionsAbstraction() + namespace type
│ └── feature.ts # createPermissionsFeature()
├── presentation/
│ └── security/
│ ├── usePermissions.ts # createUsePermissions()
│ └── HasPermission.tsx # createHasPermission()
└── Extension.tsx # RegisterFeature + Security.PermissionsThe admin schema and the API-side createPermissions schema should use the same prefix, entity IDs, and action names. This ensures the permissions emitted by the admin UI are correctly evaluated by the API.
Admin: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
API: createPermissions({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })See webiny-api-permissions for the API-side implementation.
484553b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.