CtrlK
BlogDocsLog inGet started
Tessl Logo

webiny-admin-permissions

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

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Admin Permissions

Overview

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.

Layer 1: Domain — Permission Schema

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"]
    }
  ]
});

Schema Reference

FieldTypeRequiredDescription
prefixstringYesPermission prefix (e.g., "sm")
fullAccesstrue | objectYestrue for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., { canForceUnlock: true }).
readOnlyAccessbooleanNoWhether to show a "Read-only access" option
entitiesEntityDefinition[]NoEntity definitions. Omit for binary full/no access.

Entity Definition

FieldTypeRequiredDescription
idstringYesUnique identifier for form field naming
titlestringNoDisplay title. Falls back to id.
permissionstringYesPermission name emitted (e.g., "sm.product")
scopes("full" | "own")[]YesAvailable access scopes
actionsActionDefinition[]NoActions on this entity
dependsOn{ entity: string; requires: string }NoDependency on another entity

Actions

  • { 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.

Entity Dependencies

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" }
}

Simple Apps (No Entities)

Omit entities for binary full/no access:

export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
  prefix: "ma",
  fullAccess: true
});

Layer 2: Features — DI Artifacts + Registration

Abstraction (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>;
}

Feature (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);

Extension Registration

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>
    </>
  );
};

Layer 3: Presentation — Hooks & Components

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 flag

Entity 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>

DI Injection in Features

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

PropTypeRequiredDescription
namestringYesUnique identifier for this permission renderer
titlestringYesDisplay title in the accordion header
descriptionstringNoDescription shown below the title
iconReactElementNoIcon in the accordion header
schemaPermissionSchemaOne of schema/elementAuto-generate UI from schema
elementReactElementOne of schema/elementFully custom permission UI
systembooleanNoIf true, renders before app-level permissions

File Structure

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.Permissions

Matching API-Side Permissions

The 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.

Related Skills

  • webiny-api-permissions — API-side permission checking (canRead, canEdit, etc.)
  • webiny-admin-architect — Admin architecture, headless and presentation features
  • webiny-admin-ui-extensions — Admin UI customization, decorators, config
Repository
webiny/webiny-js
Last updated
First committed

Is this your skill?

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.