Webiny-js webiny-admin-permissions

install
source · Clone the upstream repo
git clone https://github.com/webiny/webiny-js
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/webiny/webiny-js "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/user-skills/admin/admin-permissions" ~/.claude/skills/webiny-webiny-js-webiny-admin-permissions && rm -rf "$T"
manifest: skills/user-skills/admin/admin-permissions/SKILL.md
source content

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
prefix
string
YesPermission 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
NoWhether to show a "Read-only access" option
entities
EntityDefinition[]
NoEntity definitions. Omit for binary full/no access.

Entity Definition

FieldTypeRequiredDescription
id
string
YesUnique identifier for form field naming
title
string
NoDisplay title. Falls back to
id
.
permission
string
YesPermission name emitted (e.g.,
"sm.product"
)
scopes
("full" | "own")[]
YesAvailable access scopes
actions
ActionDefinition[]
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
name
string
YesUnique identifier for this permission renderer
title
string
YesDisplay title in the accordion header
description
string
NoDescription shown below the title
icon
ReactElement
NoIcon 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
NoIf
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