ra-rbac: Role-Based Access Control for React-admin apps.

react-admin ≥ 5.3.3

This module builds up on react-admin's Access Control API. It provides an implementation for authProvider.canAccess() to manage roles and fine-grained permissions, and exports alternative react-admin components that use these permissions.

Test it live in the Enterprise Edition Storybook.

Note: ra-rbac allows to show or hide UI elements in a react-admin app, which runs on the client side. However, to properly secure an app, you need to double the checks on the server-side. The exposed data is the responsibility of your back-end.

At a Glance

ra-rbac relies on an array of roles and permissions to determine what a user can do in a React-admin application. You can define permissions for pages, fields, buttons, etc. These permissions use a serialization format that is easy to understand and to maintain. You can store them in a database, in a JSON file, or in your code.

Roles and permissions are used by authProvider.canAccess() to provide fine-grained access control to the entire app.

The above demo uses the following set of permissions:

import { RoleDefinitions } from '@react-admin/ra-rbac';

const roles: RoleDefinitions = {
    accountant: [
        { action: ['list', 'show'], resource: 'products' },
        { action: 'read', resource: 'products.*' },
        { type: 'deny', action: 'read', resource: 'products.description' },
        { action: 'list', resource: 'categories' },
        { action: 'read', resource: 'categories.*' },
        { action: ['list', 'show'], resource: 'customers' },
        { action: 'read', resource: 'customers.*' },
        { action: '*', resource: 'invoices' },
    ],
    contentEditor: [
        {
            action: ['list', 'create', 'edit', 'delete', 'export'],
            resource: 'products',
        },
        { action: 'read', resource: 'products.*' },
        { type: 'deny', action: 'read', resource: 'products.stock' },
        { type: 'deny', action: 'read', resource: 'products.sales' },
        { action: 'write', resource: 'products.*' },
        { type: 'deny', action: 'write', resource: 'products.stock' },
        { type: 'deny', action: 'write', resource: 'products.sales' },
        { action: 'list', resource: 'categories' },
        { action: ['list', 'edit'], resource: 'customers' },
        { action: ['list', 'edit'], resource: 'reviews' },
    ],
    stockManager: [
        { action: ['list', 'edit', 'export'], resource: 'products' },
        { action: 'read', resource: 'products.*' },
        {
            type: 'deny',
            action: 'read',
            resource: 'products.description',
        },
        { action: 'write', resource: 'products.stock' },
        { action: 'write', resource: 'products.sales' },
        { action: 'list', resource: 'categories' },
    ],
    administrator: [{ action: '*', resource: '*' }],
};
const roles = {
    accountant: [
        { action: ["list", "show"], resource: "products" },
        { action: "read", resource: "products.*" },
        { type: "deny", action: "read", resource: "products.description" },
        { action: "list", resource: "categories" },
        { action: "read", resource: "categories.*" },
        { action: ["list", "show"], resource: "customers" },
        { action: "read", resource: "customers.*" },
        { action: "*", resource: "invoices" },
    ],
    contentEditor: [
        {
            action: ["list", "create", "edit", "delete", "export"],
            resource: "products",
        },
        { action: "read", resource: "products.*" },
        { type: "deny", action: "read", resource: "products.stock" },
        { type: "deny", action: "read", resource: "products.sales" },
        { action: "write", resource: "products.*" },
        { type: "deny", action: "write", resource: "products.stock" },
        { type: "deny", action: "write", resource: "products.sales" },
        { action: "list", resource: "categories" },
        { action: ["list", "edit"], resource: "customers" },
        { action: ["list", "edit"], resource: "reviews" },
    ],
    stockManager: [
        { action: ["list", "edit", "export"], resource: "products" },
        { action: "read", resource: "products.*" },
        {
            type: "deny",
            action: "read",
            resource: "products.description",
        },
        { action: "write", resource: "products.stock" },
        { action: "write", resource: "products.sales" },
        { action: "list", resource: "categories" },
    ],
    administrator: [{ action: "*", resource: "*" }],
};
export {};

Installation

npm install --save @react-admin/ra-rbac
# or
yarn add @react-admin/ra-rbac

Tip: ra-rbac is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

Make sure you enable auth features by setting an <Admin authProvider>, and disable anonymous access by adding the <Admin requireAuth> prop. This will ensure that react-admin waits for the authProvider response before rendering anything.

Concepts

Permission

A permission is an object that represents access to a subset of the application. It is defined by a resource (usually a noun) and an action (usually a verb), with sometimes an additional record.

Here are a few examples of permissions:

  • { action: "*", resource: "*" }: allow everything
  • { action: "read", resource: "*" }: allow read actions on all resources
  • { action: "read", resource: ["companies", "people"] }: allow read actions on a subset of resources
  • { action: ["read", "create", "edit", "export"], resource: "companies" }: allow all actions except delete on companies
  • { action: ["write"], resource: "game.score", record: { "id": "123" } }: allow write action on the score of the game with id 123

Tip: When the record field is omitted, the permission is valid for all records.

Role

A role is a string that represents a responsibility. Examples of roles include "admin", "reader", "moderator", and "guest". A user can have one or more roles.

Role Definition

A role definition is an array of permissions. It lists the operations that a user with that role can perform.

Here are a few example role definitions:

// the admin role has all the permissions
const adminRole = [{ action: '*', resource: '*' }];

// the reader role can only read content, not create, edit or delete it
const readerRole = [{ action: 'read', resource: '*' }];

// fine-grained permissions on a per resource basis
const salesRole = [
    { action: ['read', 'create', 'edit', 'export'], resource: 'companies' },
    { action: ['read', 'create', 'edit'], resource: 'people' },
    { action: ['read', 'create', 'edit', 'export'], resource: 'deals' },
    { action: ['read', 'create'], resource: 'comments' },
    ,
    { action: ['read', 'create'], resource: 'tasks' },
    { action: ['write'], resource: 'tasks.completed' },
];

// permissions can be restricted to a specific list of records, and are additive
const corrector123Role = [
    // can only grade the assignments assigned to him
    {
        action: ['read', 'export', 'edit', 'grade'],
        resource: 'assignments',
        record: { supervisor_id: '123' },
    },
    // can see the general stats page
    { action: 'read', resource: 'stats' },
    // can see the profile of every corrector
    { action: ['read'], resource: 'correctors' },
    // can edit his own profile
    { action: ['write'], resource: 'correctors', record: { id: '123' } },
];
// the admin role has all the permissions
const adminRole = [{ action: "*", resource: "*" }];

// the reader role can only read content, not create, edit or delete it
const readerRole = [{ action: "read", resource: "*" }];

// fine-grained permissions on a per resource basis
const salesRole = [
    { action: ["read", "create", "edit", "export"], resource: "companies" },
    { action: ["read", "create", "edit"], resource: "people" },
    { action: ["read", "create", "edit", "export"], resource: "deals" },
    { action: ["read", "create"], resource: "comments" },
    ,
    { action: ["read", "create"], resource: "tasks" },
    { action: ["write"], resource: "tasks.completed" },
];

// permissions can be restricted to a specific list of records, and are additive
const corrector123Role = [
    // can only grade the assignments assigned to him
    {
        action: ["read", "export", "edit", "grade"],
        resource: "assignments",
        record: { supervisor_id: "123" },
    },
    // can see the general stats page
    { action: "read", resource: "stats" },
    // can see the profile of every corrector
    { action: ["read"], resource: "correctors" },
    // can edit his own profile
    { action: ["write"], resource: "correctors", record: { id: "123" } },
];

Tip: The order of permissions isn't significant. As soon as at least one permission grants access to an action on a resource, ra-rbac grant access to it-unless there is an explicit deny.

Action

An action is a string, usually a verb, that represents an operation. Examples of actions include "read", "create", "edit", "delete", or "export".

React-admin already does page-level access control with actions like "list", "show", "edit", "create", and "delete". Ra-rbac checks additional actions in its components:

Components Action Description
<Datagrid>, <SimpleShowLayout>, <TabbedShowLayout> read Allow to view a field (or a tab)
<SimpleForm>, <TabbedForm> write Allow to edit a field (or a tab)
<List>, <ExportButton> export Allow to export data
<Datagrid> delete Allow to bulk delete data
<Edit>, <CloneButton> clone Allow to clone a record

Tip: You can also define your own actions, and implement them in your own components using react-admin's useCanAccess, or <CanAccess>.

Tip: Be sure not to confuse "show" and "read", or "edit" and "write", as they are not the same. The first operate at the page level, the second at the field level. A good mnemonic is to realize "show" and "edit" are named the same as the react-admin page they allow to control: the Show and Edit pages.

Pessimistic Strategy

Like React-admin, Ra-rbac treats permissions in a pessimistic way: while permissions are loading, react-admin doesn't render the components that require permissions, assuming that these components are restricted by default. It's only when the authProvider.canAccess() has resolved that ra-rbac renders the components.

Principle Of Least Privilege

A user with no permissions has access to nothing. By default, any restricted action is accessible to nobody. This is also called an "implicit deny".

To put it otherwise, only users with the right permissions can execute an action on a resource and a record.

Permissions are additive, each permission granting access to a subset of the application.

Record-Level Permissions

By default, a permission applies to all records of a resource.

A permission can be restricted to a specific record or a specific set of records. Setting the record field in a permission restricts the application of that permissions to records matching that criteria (using lodash isMatch).

// can view all users, without record restriction
const perm1 = { action: ['list', 'show'], resource: 'users' };
const perm2 = { action: 'read', resource: 'users.*' };
// can only edit field 'username' for user of id 123
const perm4 = { action: 'write', resource: 'users.username', record: { id: '123' } };

Only record-level components can perform record-level permissions checks. Below is the list of components that support them:

When you restrict permissions to a specific set of records, components that do not support record-level permissions (such as List Components) will ignore the record criteria and perform their checks at the resource-level only.

Explicit Deny

Some users may have access to all resources but one. Instead of having to list all the resources they have access to, you can use a special permission with the "deny" type that explicitly denies access to a resource.

const allProductsButStock = [
    { action: 'read', resource: 'products.*' },
    { type: 'deny', action: 'read', resource: 'products.stock' },
    { type: 'deny', action: 'read', resource: 'products.sales' },
];
// is equivalent to
const allProductsButStock = [
    { action: 'read', resource: 'products.thumbnail' },
    { action: 'read', resource: 'products.reference' },
    { action: 'read', resource: 'products.category_id' },
    { action: 'read', resource: 'products.width' },
    { action: 'read', resource: 'products.height' },
    { action: 'read', resource: 'products.price' },
    { action: 'read', resource: 'products.description' },
];

Tip: Deny permissions are evaluated first, no matter in which order the permissions are defined.

Setup

Define role definitions in your application code, or fetch them from the API.

export const roleDefinitions = {
    admin: [
        { action: '*', resource: '*' }
    ],
    reader: [
        { action: ['list', 'show', 'export'], resource: '*' }
        { action: 'read', resource: 'posts.*' }
        { action: 'read', resource: 'comments.*' }
    ],
    accounting: [
        { action: '*', resource: 'sales' },
    ],
};

The user roles and permissions should be returned upon login. The authProvider should store the permissions in memory, or in localStorage. This allows authProvider.canAccess() to read the permissions from localStorage.

import { getPermissionsFromRoles } from '@react-admin/ra-rbac';
import { roleDefinitions } from './roleDefinitions';

const authProvider = {
    login: async ({ username, password }) => {
        const request = new Request('https://mydomain.com/authenticate', {
            method: 'POST',
            body: JSON.stringify({ username, password }),
            headers: new Headers({ 'Content-Type': 'application/json' }),
        });
        const response = await fetch(request);
            if (response.status < 200 || response.status >= 300) {
                throw new Error(response.statusText);
            }
        const { user: { roles, permissions }} = await response.json();
        // merge the permissions from the roles with the extra permissions
        const permissions = getPermissionsFromRoles({
            roleDefinitions,
            userPermissions,
            userRoles
        });
        localStorage.setItem('permissions', JSON.stringify(permissions));
    },
    // ...
};
import { getPermissionsFromRoles } from "@react-admin/ra-rbac";
import { roleDefinitions } from "./roleDefinitions";

const authProvider = {
    login: async ({ username, password }) => {
        const request = new Request("https://mydomain.com/authenticate", {
            method: "POST",
            body: JSON.stringify({ username, password }),
            headers: new Headers({ "Content-Type": "application/json" }),
        });
        const response = await fetch(request);
        if (response.status < 200 || response.status >= 300) {
            throw new Error(response.statusText);
        }
        const {
            user: { roles, permissions },
        } = await response.json();
        // merge the permissions from the roles with the extra permissions
        const permissions = getPermissionsFromRoles({
            roleDefinitions,
            userPermissions,
            userRoles,
        });
        localStorage.setItem("permissions", JSON.stringify(permissions));
    },
    // ...
};

Then, use these permissions in authProvider.canAccess():

import { canAccessWithPermissions } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ resource, action, record }) => {
        const permissions = JSON.parse(localStorage.getItem('permissions'));
        // check if the user can access the resource and action
        return canAccessWithPermissions({ permissions, resource, action, record });
    },
};
import { canAccessWithPermissions } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ resource, action, record }) => {
        const permissions = JSON.parse(localStorage.getItem("permissions"));
        // check if the user can access the resource and action
        return canAccessWithPermissions({ permissions, resource, action, record });
    },
};

Tip: If canAccess needs to call the server every time, check out the Performance section below.

getPermissionsFromRoles

This function returns an array of user permissions based on a role definition, a list of roles, and a list of user permissions. It merges the permissions defined in roleDefinitions for the current user's roles (userRoles) with the extra userPermissions.

// static role definitions (usually in the app code)
const roleDefinitions = {
    admin: [
        { action: '*', resource: '*' }
    ],
    reader: [
        { action: ['list', 'show', 'export'], resource: '*' }
        { action: 'read', resource: 'posts.*' }
        { action: 'read', resource: 'comments.*' }
    ],
    accounting: [
        { action: '*', resource: 'sales' },
    ],
};

const permissions = getPermissionsFromRoles({    
    roleDefinitions,
    // roles of the current user (usually returned by the server upon login)
    userRoles: ['reader'],
    // extra permissions for the current user (usually returned by the server upon login)
    userPermissions: [
        { action: 'list', resource: 'sales'},
    ],
});
// permissions = [
//  { action: ['list', 'show', 'export'], resource: '*' },
//  { action: 'read', resource: 'posts.*' },
//  { action: 'read', resource: 'comments.*' },
//  { action: 'list', resource: 'sales' },
// ];

This function takes an object as argument with the following fields:

  • roleDefinitions: a dictionary containing the role definition for each role
  • userRoles (optional): an array of roles (admin, reader...) for the current user
  • userPermissions (optional): an array of permissions for the current user

canAccessWithPermissions

canAccessWithPermissions is a helper that facilitates the authProvider.canAccess() method implementation:

import { canAccessWithPermissions } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, resource, record }) => {    
        const permissions = JSON.parse(localStorage.getItem('permissions'));
        return canAccessWithPermissions({
            permissions,
            action,
            resource,
            record,
        });
    }
};
import { canAccessWithPermissions } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, resource, record }) => {
        const permissions = JSON.parse(localStorage.getItem("permissions"));
        return canAccessWithPermissions({
            permissions,
            action,
            resource,
            record,
        });
    },
};

canAccessWithPermissions expects the permissions to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the getPermissionsFromRoles function.

<List>

Replacement for react-admin's <List> that adds RBAC control to actions, and to the default export function.

  • Users must have the 'create' permission on the resource to see the <CreateButton>.
  • Users must have the 'export' permission on the resource to see the <ExportButton>.
  • Users must have the 'read' permission on a resource column to see it in the export:
{ action: "read", resource: `${resource}.${source}` }.
// 
{ action: "read", resource: `${resource}.*` }.
import { List } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async () =>
        canAccessWithPermissions({
            permissions: [
                { action: 'list', resource: 'products' },
                { action: 'export', resource: 'products' },
                // actions 'create' and 'delete' are missing
                { action: 'read', resource: 'products.name' },
                { action: 'read', resource: 'products.description' },
                { action: 'read', resource: 'products.price' },
                { action: 'read', resource: 'products.category' },
                // resource 'products.stock' is missing
            ],
            action,
            resource,
            record
        }),
};

export const PostList = () => (
    <List exporter={exporter}>
        {/*...*/}
    </List>
);
// Users will see the Export action on top of the list, but not the Create action.
// Users will only see the authorized columns when clicking on the export button.
import { List } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async () =>
        canAccessWithPermissions({
            permissions: [
                { action: "list", resource: "products" },
                { action: "export", resource: "products" },
                // actions 'create' and 'delete' are missing
                { action: "read", resource: "products.name" },
                { action: "read", resource: "products.description" },
                { action: "read", resource: "products.price" },
                { action: "read", resource: "products.category" },
                // resource 'products.stock' is missing
            ],
            action,
            resource,
            record,
        }),
};

export const PostList = () => <List exporter={exporter}>{/*...*/}</List>;
// Users will see the Export action on top of the list, but not the Create action.
// Users will only see the authorized columns when clicking on the export button.

Tip: If you need a custom exporter, you can use useExporterWithAccessControl to apply access control to the exported records:

import { List, useExporterWithAccessControl } from '@ra-enterprise/ra-rbac';
import { myExporter } from './myExporter';

export const PostList = () => {
    const exporter = useExporterWithAccessControl({ exporter: myExporter })
    return (
        <List exporter={exporter}>
            {/*...*/}
        </List>
    );
}

import { List, useExporterWithAccessControl } from "@ra-enterprise/ra-rbac";
import { myExporter } from "./myExporter";

export const PostList = () => {
    const exporter = useExporterWithAccessControl({ exporter: myExporter });
    return <List exporter={exporter}>{/*...*/}</List>;
};

Tip: This <List> component relies on the <ListActions> component below.

<ListActions>

Replacement for react-admin's <ListAction> that adds RBAC control to actions

  • Users must have the 'create' permission on the resource to see the <CreateButton>.
  • Users must have the 'export' permission on the resource to see the <ExportButton>.
import { List } from 'react-admin';
import { ListActions } from '@react-admin/ra-rbac';

export const PostList = () => <List actions={<ListActions />}>...</List>;

<ExportButton>

Replacement for react-admin's <ExportButton> that checks users have the 'export' permission before rendering. Use it if you want to provide your own actions to the <List>:

import { CreateButton, List, TopToolbar } from 'react-admin';
import { ExportButton } from '@react-admin/ra-rbac';

const PostListActions = () => (
    <TopToolbar>
        <PostFilter context="button" />
        <CreateButton />
        <ExportButton />
    </TopToolbar>
);

export const PostList = () => (
    <List actions={<PostListActions />}>
        {/* ... */}
    </List>
);
import { CreateButton, List, TopToolbar } from "react-admin";
import { ExportButton } from "@react-admin/ra-rbac";

const PostListActions = () => (
    <TopToolbar>
        <PostFilter context="button" />
        <CreateButton />
        <ExportButton />
    </TopToolbar>
);

export const PostList = () => <List actions={<PostListActions />}>{/* ... */}</List>;

It accepts the following props in addition to the default <ExportButton> props:

Prop Required Type Default Description
accessDenied Optional ReactNode null The content to display when users don't have the 'export' permission
action Optional String "export" The action to call authProvider.canAccess with
authorizationError Optional ReactNode null The content to display when an error occurs while checking permission

Tip: Don't forget to give read permissions on all the fields you want to allow in exports

{ action: "read", resource: `${resource}.${source}` }.
// or
{ action: "read", resource: `${resource}.*` }.

<Datagrid>

Alternative to react-admin's <Datagrid> that adds RBAC control to columns.

  • Users must have the 'delete' permission on the resource to see the <BulkExportButton>.
  • Users must have the 'read' permission on a resource column to see it in the export:
{ action: "read", resource: `${resource}.${source}` }.
// or
{ action: "read", resource: `${resource}.*` }.

Also, the rowClick prop is automatically set depending on the user props:

  • "edit" if the user has the permission to edit the resource
  • "show" if the user doesn't have the permission to edit the resource but has the permission to show it
  • empty otherwise
import { canAccessWithPermissions, List, Datagrid } from '@react-admin/ra-rbac';
import {
    ImageField,
    TextField,
    ReferenceField,
    NumberField,
} from 'react-admin';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: 'list', resource: 'products' },
                { action: 'read', resource: 'products.thumbnail' },
                { action: 'read', resource: 'products.reference' },
                { action: 'read', resource: 'products.category_id' },
                { action: 'read', resource: 'products.width' },
                { action: 'read', resource: 'products.height' },
                { action: 'read', resource: 'products.price' },
                { action: 'read', resource: 'products.description' },
            ],
            action,
            record,
            resource
        }),
};

const ProductList = () => (
    <List>
        {/* ra-rbac Datagrid */}
        <Datagrid>
            <ImageField source="thumbnail" />
            <TextField source="reference" />
            <ReferenceField source="category_id" reference="categories">
                <TextField source="name" />
            </ReferenceField>
            <NumberField source="width" />
            <NumberField source="height" />
            <NumberField source="price" />
            <TextField source="description" />
            {/** these two columns are not visible to the user **/}
            <NumberField source="stock" />
            <NumberField source="sales" />
        </Datagrid>
    </List>
);
import { canAccessWithPermissions, List, Datagrid } from "@react-admin/ra-rbac";
import { ImageField, TextField, ReferenceField, NumberField } from "react-admin";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: "list", resource: "products" },
                { action: "read", resource: "products.thumbnail" },
                { action: "read", resource: "products.reference" },
                { action: "read", resource: "products.category_id" },
                { action: "read", resource: "products.width" },
                { action: "read", resource: "products.height" },
                { action: "read", resource: "products.price" },
                { action: "read", resource: "products.description" },
            ],
            action,
            record,
            resource,
        }),
};

const ProductList = () => (
    <List>
        {/* ra-rbac Datagrid */}
        <Datagrid>
            <ImageField source="thumbnail" />
            <TextField source="reference" />
            <ReferenceField source="category_id" reference="categories">
                <TextField source="name" />
            </ReferenceField>
            <NumberField source="width" />
            <NumberField source="height" />
            <NumberField source="price" />
            <TextField source="description" />
            {/** these two columns are not visible to the user **/}
            <NumberField source="stock" />
            <NumberField source="sales" />
        </Datagrid>
    </List>
);

Tip: Adding the 'read' permission on the resource itself doesn't grant the 'read' permission on the columns. If you want a user to see all possible columns, add the 'read' permission on columns using a wildcard:

{ action: "read", resource: "products.*" }.

<SimpleShowLayout>

Alternative to react-admin's <SimpleShowLayout> that adds RBAC control to fields

To see a column, the user must have the permission to read the resource column:

{ action: "read", resource: `${resource}.${source}` }
// Or
{ action: "read", resource: `${resource}.*` }
import { SimpleShowLayout } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ['list', 'show'], resource: 'products' },
                { action: 'read', resource: 'products.reference' },
                { action: 'read', resource: 'products.width' },
                { action: 'read', resource: 'products.height' },
                // 'products.description' is missing
                // 'products.image' is missing
                { action: 'read', resource: 'products.thumbnail' },
                // 'products.stock' is missing
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <SimpleShowLayout>
            {/* <-- RBAC SimpleShowLayout */}
            <TextField source="reference" />
            <TextField source="width" />
            <TextField source="height" />
            {/* not displayed */}
            <TextField source="description" />
            {/* not displayed */}
            <TextField source="image" />
            <TextField source="thumbnail" />
            {/* not displayed */}
            <TextField source="stock" />
        </SimpleShowLayout>
    </Show>
);
import { SimpleShowLayout } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ["list", "show"], resource: "products" },
                { action: "read", resource: "products.reference" },
                { action: "read", resource: "products.width" },
                { action: "read", resource: "products.height" },
                // 'products.description' is missing
                // 'products.image' is missing
                { action: "read", resource: "products.thumbnail" },
                // 'products.stock' is missing
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <SimpleShowLayout>
            {/* <-- RBAC SimpleShowLayout */}
            <TextField source="reference" />
            <TextField source="width" />
            <TextField source="height" />
            {/* not displayed */}
            <TextField source="description" />
            {/* not displayed */}
            <TextField source="image" />
            <TextField source="thumbnail" />
            {/* not displayed */}
            <TextField source="stock" />
        </SimpleShowLayout>
    </Show>
);

<TabbedShowLayout>

Replacement for react-admin's <TabbedShowLayout> that only renders a tab if the user has the right permissions.

Use it in conjunction with <TabbedShowLayout.Tab> and add a name prop to the Tab to define the resource on which the user needs to have the 'read' permissions for.

Tip: <TabbedShowLayout.Tab> also allows to only render the child fields for which the user has the 'read' permissions.

import { Show, TextField } from 'react-admin';
import { TabbedShowLayout } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ['list', 'show'], resource: 'products' },
                { action: 'read', resource: 'products.reference' },
                { action: 'read', resource: 'products.width' },
                { action: 'read', resource: 'products.height' },
                { action: 'read', resource: 'products.thumbnail' },
                { action: 'read', resource: 'products.tab.description' },
                // 'products.tab.stock' is missing
                { action: 'read', resource: 'products.tab.images' },
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <TabbedShowLayout>
            <TabbedShowLayout.Tab label="Description" name="description">
                <TextField source="reference" />
                <TextField source="width" />
                <TextField source="height" />
                <TextField source="description" />
            </TabbedShowLayout.Tab>
            {/* Tab Stock is not displayed */}
            <TabbedShowLayout.Tab label="Stock" name="stock">
                <TextField source="stock" />
            </TabbedShowLayout.Tab>
            <TabbedShowLayout.Tab label="Images" name="images">
                <TextField source="image" />
                <TextField source="thumbnail" />
            </TabbedShowLayout.Tab>
        </TabbedShowLayout>
    </Show>
);
import { Show, TextField } from "react-admin";
import { TabbedShowLayout } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ["list", "show"], resource: "products" },
                { action: "read", resource: "products.reference" },
                { action: "read", resource: "products.width" },
                { action: "read", resource: "products.height" },
                { action: "read", resource: "products.thumbnail" },
                { action: "read", resource: "products.tab.description" },
                // 'products.tab.stock' is missing
                { action: "read", resource: "products.tab.images" },
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <TabbedShowLayout>
            <TabbedShowLayout.Tab label="Description" name="description">
                <TextField source="reference" />
                <TextField source="width" />
                <TextField source="height" />
                <TextField source="description" />
            </TabbedShowLayout.Tab>
            {/* Tab Stock is not displayed */}
            <TabbedShowLayout.Tab label="Stock" name="stock">
                <TextField source="stock" />
            </TabbedShowLayout.Tab>
            <TabbedShowLayout.Tab label="Images" name="images">
                <TextField source="image" />
                <TextField source="thumbnail" />
            </TabbedShowLayout.Tab>
        </TabbedShowLayout>
    </Show>
);

<TabbedShowLayout.Tab>

Replacement for react-admin's <TabbedShowLayout.Tab> that only renders a tab and its content if the user has the right permissions.

Add a name prop to the Tab to define the resource on which the user needs to have the 'read' permissions for.

<TabbedShowLayout.Tab> also only renders the child fields for which the user has the 'read' permissions.

import { Show, TextField } from 'react-admin';
import { TabbedShowLayout } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ['list', 'show'], resource: 'products' },
                { action: 'read', resource: 'products.reference' },
                { action: 'read', resource: 'products.width' },
                { action: 'read', resource: 'products.height' },
                // 'products.description' is missing
                { action: 'read', resource: 'products.thumbnail' },
                // 'products.image' is missing
                { action: 'read', resource: 'products.tab.description' },
                // 'products.tab.stock' is missing
                { action: 'read', resource: 'products.tab.images' },
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <TabbedShowLayout>
            <TabbedShowLayout.Tab label="Description" name="description">
                <TextField source="reference" />
                <TextField source="width" />
                <TextField source="height" />
                {/* Field Description is not displayed */}
                <TextField source="description" />
            </TabbedShowLayout.Tab>
            {/* Tab Stock is not displayed */}
            <TabbedShowLayout.Tab label="Stock" name="stock">
                <TextField source="stock" />
            </TabbedShowLayout.Tab>
            <TabbedShowLayout.Tab label="Images" name="images">
                {/* Field Image is not displayed */}
                <TextField source="image" />
                <TextField source="thumbnail" />
            </TabbedShowLayout.Tab>
        </TabbedShowLayout>
    </Show>
);
import { Show, TextField } from "react-admin";
import { TabbedShowLayout } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ["list", "show"], resource: "products" },
                { action: "read", resource: "products.reference" },
                { action: "read", resource: "products.width" },
                { action: "read", resource: "products.height" },
                // 'products.description' is missing
                { action: "read", resource: "products.thumbnail" },
                // 'products.image' is missing
                { action: "read", resource: "products.tab.description" },
                // 'products.tab.stock' is missing
                { action: "read", resource: "products.tab.images" },
            ],
            action,
            record,
            resource,
        }),
};

const ProductShow = () => (
    <Show>
        <TabbedShowLayout>
            <TabbedShowLayout.Tab label="Description" name="description">
                <TextField source="reference" />
                <TextField source="width" />
                <TextField source="height" />
                {/* Field Description is not displayed */}
                <TextField source="description" />
            </TabbedShowLayout.Tab>
            {/* Tab Stock is not displayed */}
            <TabbedShowLayout.Tab label="Stock" name="stock">
                <TextField source="stock" />
            </TabbedShowLayout.Tab>
            <TabbedShowLayout.Tab label="Images" name="images">
                {/* Field Image is not displayed */}
                <TextField source="image" />
                <TextField source="thumbnail" />
            </TabbedShowLayout.Tab>
        </TabbedShowLayout>
    </Show>
);

<SimpleForm>

Alternative to react-admin's <SimpleForm> that shows/hides inputs based on roles and permissions.

To see an input, the user must have the permission to write the resource field:

{ action: "write", resource: `${resource}.${source}` }

<SimpleForm> also renders the delete button only if the user has the 'delete' permission.

import { Edit, TextInput } from 'react-admin';
import { SimpleForm } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                // 'delete' is missing
                { action: ['list', 'edit'], resource: 'products' },
                { action: 'write', resource: 'products.reference' },
                { action: 'write', resource: 'products.width' },
                { action: 'write', resource: 'products.height' },
                // 'products.description' is missing
                { action: 'write', resource: 'products.thumbnail' },
                // 'products.image' is missing
            ]
            action,
            record,
            resource,
        }),
};

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="reference" />
            <TextInput source="width" />
            <TextInput source="height" />
            {/* not displayed */}
            <TextInput source="description" />
            {/* not displayed */}
            <TextInput source="image" />
            <TextInput source="thumbnail" />
            {/* no delete button */}
        </SimpleForm>
    </Edit>
);
import { Edit, TextInput } from "react-admin";
import { SimpleForm } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                // 'delete' is missing
                { action: ["list", "edit"], resource: "products" },
                { action: "write", resource: "products.reference" },
                { action: "write", resource: "products.width" },
                { action: "write", resource: "products.height" },
                // 'products.description' is missing
                { action: "write", resource: "products.thumbnail" },
                // 'products.image' is missing
            ],
            action,
            record,
            resource,
        }),
};

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="reference" />
            <TextInput source="width" />
            <TextInput source="height" />
            {/* not displayed */}
            <TextInput source="description" />
            {/* not displayed */}
            <TextInput source="image" />
            <TextInput source="thumbnail" />
            {/* no delete button */}
        </SimpleForm>
    </Edit>
);

<TabbedForm>

Replacement for react-admin's <TabbedForm> that adds RBAC control to the delete button (conditioned by the 'delete' action) and only renders a tab if the user has the right permissions.

Use in conjunction with <TabbedForm.Tab> and add a name prop to the Tab to define the resource on which the user needs to have the 'write' permissions for.

Tip: <TabbedForm.Tab> also allows to only render the child inputs for which the user has the 'write' permissions.

import { Edit, TextInput } from 'react-admin';
import { TabbedForm } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                // action 'delete' is missing
                { action: ['list', 'edit'], resource: 'products' },
                { action: 'write', resource: 'products.reference' },
                { action: 'write', resource: 'products.width' },
                { action: 'write', resource: 'products.height' },
                { action: 'write', resource: 'products.thumbnail' },
                { action: 'write', resource: 'products.tab.description' },
                // tab 'stock' is missing
                { action: 'write', resource: 'products.tab.images' },
            ],
            action,
            record,
            resource,
        }),
};

const ProductEdit = () => (
    <Edit>
        <TabbedForm>
            <TabbedForm.Tab label="Description" name="description">
                <TextInput source="reference" />
                <TextInput source="width" />
                <TextInput source="height" />
                <TextInput source="description" />
            </TabbedForm.Tab>
            {/* the "Stock" tab is not displayed */}
            <TabbedForm.Tab label="Stock" name="stock">
                <TextInput source="stock" />
            </TabbedForm.Tab>
            <TabbedForm.Tab label="Images" name="images">
                <TextInput source="image" />
                <TextInput source="thumbnail" />
            </TabbedForm.Tab>
            {/* the "Delete" button is not displayed */}
        </TabbedForm>
    </Edit>
);

<TabbedForm.Tab>

Replacement for react-admin's <TabbedForm.Tab> that only renders a tab and its content if the user has the right permissions.

Add a name prop to the Tab to define the resource on which the user needs to have the 'write' permissions for.

<TabbedForm.Tab> also only renders the child inputs for which the user has the 'write' permissions.

import { Edit, TextInput } from 'react-admin';
import { TabbedForm } from '@react-admin/ra-rbac';

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ['list', 'edit'], resource: 'products' },
                { action: 'write', resource: 'products.reference' },
                { action: 'write', resource: 'products.width' },
                { action: 'write', resource: 'products.height' },
                // 'products.description' is missing
                { action: 'write', resource: 'products.thumbnail' },
                // 'products.image' is missing
                { action: 'write', resource: 'products.tab.description' },
                // 'products.tab.stock' is missing
                { action: 'write', resource: 'products.tab.images' },
            ],
            action,
            record,
            resource,
        })
};

const ProductEdit = () => (
    <Edit>
        <TabbedForm>
            <TabbedForm.Tab label="Description" name="description">
                <TextInput source="reference" />
                <TextInput source="width" />
                <TextInput source="height" />
                {/* Input Description is not displayed */}
                <TextInput source="description" />
            </TabbedForm.Tab>
            {/* Input Stock is not displayed */}
            <TabbedForm.Tab label="Stock" name="stock">
                <TextInput source="stock" />
            </TabbedForm.Tab>
            <TabbedForm.Tab label="Images" name="images">
                {/* Input Image is not displayed */}
                <TextInput source="image" />
                <TextInput source="thumbnail" />
            </TabbedForm.Tab>
        </TabbedForm>
    </Edit>
);
import { Edit, TextInput } from "react-admin";
import { TabbedForm } from "@react-admin/ra-rbac";

const authProvider = {
    // ...
    canAccess: async ({ action, record, resource }) =>
        canAccessWithPermissions({
            permissions: [
                { action: ["list", "edit"], resource: "products" },
                { action: "write", resource: "products.reference" },
                { action: "write", resource: "products.width" },
                { action: "write", resource: "products.height" },
                // 'products.description' is missing
                { action: "write", resource: "products.thumbnail" },
                // 'products.image' is missing
                { action: "write", resource: "products.tab.description" },
                // 'products.tab.stock' is missing
                { action: "write", resource: "products.tab.images" },
            ],
            action,
            record,
            resource,
        }),
};

const ProductEdit = () => (
    <Edit>
        <TabbedForm>
            <TabbedForm.Tab label="Description" name="description">
                <TextInput source="reference" />
                <TextInput source="width" />
                <TextInput source="height" />
                {/* Input Description is not displayed */}
                <TextInput source="description" />
            </TabbedForm.Tab>
            {/* Input Stock is not displayed */}
            <TabbedForm.Tab label="Stock" name="stock">
                <TextInput source="stock" />
            </TabbedForm.Tab>
            <TabbedForm.Tab label="Images" name="images">
                {/* Input Image is not displayed */}
                <TextInput source="image" />
                <TextInput source="thumbnail" />
            </TabbedForm.Tab>
        </TabbedForm>
    </Edit>
);

<CloneButton>

Replacement for react-admin's <CloneButton> that checks users have the 'clone' permission before rendering. Use it if you want to provide your own actions to the <Edit>:

import { Edit, TopToolbar } from 'react-admin';
import { CloneButton } from '@react-admin/ra-rbac';

const PostEditActions = () => (
    <TopToolbar>
        <CloneButton />
    </TopToolbar>
);

export const PostEdit = () => (
    <Edit actions={<PostEditActions />}>
        {/* ... */}
    </Edit>
);
import { Edit, TopToolbar } from "react-admin";
import { CloneButton } from "@react-admin/ra-rbac";

const PostEditActions = () => (
    <TopToolbar>
        <CloneButton />
    </TopToolbar>
);

export const PostEdit = () => <Edit actions={<PostEditActions />}>{/* ... */}</Edit>;

It accepts the following props in addition to the default <CloneButton> props:

Prop Required Type Default Description
accessDenied Optional ReactNode null The content to display when users don't have the 'clone' permission
action Optional String "clone" The action to call authProvider.canAccess with
authorizationError Optional ReactNode null The content to display when an error occurs while checking permission

If you want to add custom pages to the menu, you can use ra-rbac's <Menu> component. It will only display the menu item if the user has access to the specified action and resource.

import { Menu } from '@react-admin/ra-rbac';

export const MyMenu = () => (
    <Menu>
        {/* Resource menu items already have access control built-in */}
        <Menu.ResourceItems />
        {/* For custom menu items, you can specify a resource and action */}
        <Menu.Item
            to="/products"
            primaryText="Products"
            resource="products"
            action="list"
        />
        {/* this menu item will render for all users */}
        <Menu.Item to="/preferences" primaryText="Preferences" />
    </Menu>
);
import { Menu } from "@react-admin/ra-rbac";

export const MyMenu = () => (
    <Menu>
        {/* Resource menu items already have access control built-in */}
        <Menu.ResourceItems />
        {/* For custom menu items, you can specify a resource and action */}
        <Menu.Item to="/products" primaryText="Products" resource="products" action="list" />
        {/* this menu item will render for all users */}
        <Menu.Item to="/preferences" primaryText="Preferences" />
    </Menu>
);

Performance

authProvider.canAccess() can return a promise, which in theory allows to rely on the authentication server for permissions. The downside is that this slows down the app a great deal, as each page may contain dozens of calls to these methods.

In practice, your authProvider should use short-lived sessions, and refresh the permissions only when the session ends. JSON Web tokens (JWT) work that way.

Here is an example of an authProvider that stores the permissions in memory, and refreshes them only every 5 minutes:

import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-rbac';

let permissions; // memory cache
let permissionsExpiresAt = 0;
const getPermissions = () => {
    const request = new Request('https://mydomain.com/permissions', {
        headers: new Headers({
            Authorization: `Bearer ${localStorage.getItem('token')}`,
        }),
    });
    return fetch(request)
        .then(res => resp.json())
        .then(data => {
            permissions = data.permissions;
            permissionsExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes
        });
};

let roleDefinitions; // memory cache
let rolesExpiresAt = 0;
const getRoles = () => {
    const request = new Request('https://mydomain.com/roles', {
        headers: new Headers({
            Authorization: `Bearer ${localStorage.getItem('token')}`,
        }),
    });
    return fetch(request)
        .then(res => resp.json())
        .then(data => {
            roleDefinitions = data.roles;
            rolesExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes
        });
};

const authProvider = {
    login: ({ username, password }) => {
        const request = new Request('https://mydomain.com/authenticate', {
            method: 'POST',
            body: JSON.stringify({ username, password }),
            headers: new Headers({ 'Content-Type': 'application/json' }),
        });
        return fetch(request)
            .then(response => {
                if (response.status < 200 || response.status >= 300) {
                    throw new Error(response.statusText);
                }
                return response.json();
            })
            .then(data => {
                localStorage.setItem('token', JSON.stringify(data.token));
                localStorage.setItem('userRoles', JSON.stringify(data.roles));
            });
    },
    // ...
    canAccess: async ({ action, record, resource }) => {
        if (Date.now() > rolesExpiresAt) {
            await getRoles();
        }
        if (Date.now() > permissionsExpiresAt) {
            await getPermissions();
        }
        return canAccessWithPermissions({
            permissions: getPermissionsFromRoles({
                roleDefinitions,
                userPermissions: permissions,
                userRoles: localStorage.getItem('userRoles'),
            },
            action,
            record,
            resource,
        });
    },
};
import { canAccessWithPermissions, getPermissionsFromRoles } from "@react-admin/ra-rbac";

let permissions; // memory cache
let permissionsExpiresAt = 0;
const getPermissions = () => {
    const request = new Request("https://mydomain.com/permissions", {
        headers: new Headers({
            Authorization: `Bearer ${localStorage.getItem("token")}`,
        }),
    });
    return fetch(request)
        .then((res) => resp.json())
        .then((data) => {
            permissions = data.permissions;
            permissionsExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes
        });
};

let roleDefinitions; // memory cache
let rolesExpiresAt = 0;
const getRoles = () => {
    const request = new Request("https://mydomain.com/roles", {
        headers: new Headers({
            Authorization: `Bearer ${localStorage.getItem("token")}`,
        }),
    });
    return fetch(request)
        .then((res) => resp.json())
        .then((data) => {
            roleDefinitions = data.roles;
            rolesExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes
        });
};

const authProvider = {
    login: ({ username, password }) => {
        const request = new Request("https://mydomain.com/authenticate", {
            method: "POST",
            body: JSON.stringify({ username, password }),
            headers: new Headers({ "Content-Type": "application/json" }),
        });
        return fetch(request)
            .then((response) => {
                if (response.status < 200 || response.status >= 300) {
                    throw new Error(response.statusText);
                }
                return response.json();
            })
            .then((data) => {
                localStorage.setItem("token", JSON.stringify(data.token));
                localStorage.setItem("userRoles", JSON.stringify(data.roles));
            });
    },
    // ...
    canAccess: async ({ action, record, resource }) => {
        if (Date.now() > rolesExpiresAt) {
            await getRoles();
        }
        if (Date.now() > permissionsExpiresAt) {
            await getPermissions();
        }
        return canAccessWithPermissions({
            permissions: getPermissionsFromRoles(
                {
                    roleDefinitions,
                    userPermissions: permissions,
                    userRoles: localStorage.getItem("userRoles"),
                },
                action,
                record,
                resource
            ),
        });
    },
};

CHANGELOG

v6.0.0

2024-12-09

React-admin v5.3 now implements access control for page components (list, show, etc). ra-rbac v6 leverages react-admin v5.3 to provide fine-grained access control based on a list of permissions, and alternative components with built-in access control (<Datagrid>, <SimpleShowLayout>, etc). This means you'll have to update some of your imports (see below).

  • Bump dependency on react-admin to 5.3.0
  • Add <ExportButton> and <CloneButton> components with built-in access control
  • Remove components ported to react-admin:
    • <Resource>
    • <Edit>
    • <Show>
    • <IfCanAccess>
    • useAuthenticated
    • useCanAccess
  • Remove translations as they are now handled by react-admin

Breaking changes

  • @ra-enterprise/ra-rbac now leverages authProvider.canAccess() instead of authProvider.getPermissions(), so you need to move authorization logic in your authProvider:
-import { getPermissionsFromRoles } from '@react-admin/ra-rbac';
+import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-rbac';

const authProvider = {
    login: ({ username, password }) => {
        const request = new Request('https://mydomain.com/authenticate', {
            method: 'POST',
            body: JSON.stringify({ username, password }),
            headers: new Headers({ 'Content-Type': 'application/json' }),
        });
        return fetch(request)
            .then(response => {
                if (response.status < 200 || response.status >= 300) {
                    throw new Error(response.statusText);
                }
                return response.json();
            })
            .then(data => {
                const permissions = getPermissionsFromRoles({
                    roleDefinitions: data.roleDefinitions,
                    userPermissions: data.user.permissions,
                    userRoles: data.user.roles,
                });
                localStorage.setItem(
                    'permissions',
                    JSON.stringify(permissions)
                );
            });
    },
    // ...
-    getPermissions: () => {
-        const permissions = JSON.parse(localStorage.getItem('permissions'));
-        return Promise.resolve(permissions);
-    },
+    canAccess: ({ resource, action, record }) => {
+        const permissions = JSON.parse(localStorage.getItem('permissions'));
+        return canAccessWithPermissions({ permissions, resource, action, record });
    },
};
  • The canAccess utility function has been renamed canAccessWithPermissions. We still export a canAccess alias for it, but we recommend you update your imports:
-import { canAccess } from '@react-admin/ra-rbac';
+import { canAccessWithPermissions } from '@react-admin/ra-rbac';

-canAccess({
+canAccessWithPermissions({
    permissions: [
        { action: 'read', resource: 'user' },
        { action: ['read', 'edit', 'create', 'delete'], resource: 'posts' },
    ],
    action: 'edit',
    resource: 'posts',
}); // true
  • Resource is no longer exported from @ra-enterprise/ra-rbac, as its functionality (hiding resources for unauthorized users) has been ported to react-admin. Use the regular <Resource> from react-admin to get the same functionality:
-import { Admin } from 'react-admin';
-import { Resource } from '@react-admin/ra-rbac';
+import { Admin, Resource } from 'react-admin';

import users from './users';
import dataProvider from './dataProvider';
import authProvider from './authProvider';

const App = () => (
    <Admin dataProvider={dataProvider} authProvider={authProvider}>
        <Resource  name="users" {...users} />
    </Admin>
);
  • Edit and Show are no longer exported from @ra-enterprise/ra-rbac, as their functionality has been ported to react-admin. Use the regular <Edit> and <Show> components from react-admin instead:
-import { TextField, TextInput } from 'react-admin';
+import { Edit, Show, TextField, TextInput } from 'react-admin';
-import { Edit, Show, SimpleForm, SimpleShowLayout } from '@react-admin/ra-rbac';
+import { SimpleForm, SimpleShowLayout } from '@react-admin/ra-rbac';

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="reference" />
        </SimpleForm>
    </Edit>
);

const ProductShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="reference" />
        </SimpleShowLayout>
    </Show>
);

Tip: You no longer have to redirect users to a custom /access-denied route for default react-admin views as they already do it.

  • IfCanAccess is no longer exported from @ra-enterprise/ra-rbac, as it was reimplemented in react-admin. Use <CanAccess> from react-admin instead. It has a similar syntax, except the fallback prop was renamed to accessDenied:
-import { DeleteButton, ShowButton } from 'react-admin';
-import { IfCanAccess } from '@react-admin/ra-rbac';
+import { CanAccess, DeleteButton, ShowButton } from 'react-admin';
import { MyCustomButton } from './MyCustomButton';

const RecordToolbar = () => (
    <Toolbar>
-        <IfCanAccess action="edit">
+        <CanAccess action="edit">
            <MyCustomButton />
-        </IfCanAccess>
+        </CanAccess>
    </Toolbar>
);

// in src/AccessDenied.tsx
export const AccessDenied = () => (
    <Typography>You don't have the required permissions to access this page.</Typography>
);

// in src/posts/PostCreate.tsx
-import { Create, SimpleForm, TextInput } from 'react-admin';
-import { IfCanAccess } from '@react-admin/ra-rbac';
+import { CanAccess, Create, SimpleForm, TextInput } from 'react-admin';
import { Navigate } from 'react-router-dom';

export const PostCreate = () => (
-    <IfCanAccess action="create" fallback={<AccessDenied />}>
+    <CanAccess action="create" accessDenied={<AccessDenied />}>
        <Create>
            <SimpleForm>
                <TextInput source="title" />
            </SimpleForm>
        </Create>
-    </IfCanAccess>
+    </IfCanAccess>
);

Tip: You no longer have to check permissions for built-in react-admin buttons such as EditButton, CreateButton or DeleteButton as they already do it.

  • useAuthenticated is no longer exported from @ra-enterprise/ra-rbac, as its feature (pessimistic auth check) has been ported to react-admin. Use the same hook from react-admin instead. Note that React-admin useAuthenticated redirects anonymous users to the login page by default. Pass the logoutOnFailure option to display custom content if you need:
-import { useAuthenticated } from '@react-admin/ra-rbac';
+import { useAuthenticated } from 'react-admin';

const SecretData = () => {
-    const { authenticated } = useAuthenticated();
+    const { authenticated } = useAuthenticated({ logoutOnFailure: false });
    return authenticated ? null : <span>For your eyes only</span>;
};
  • useCanAccess is no longer exported from @ra-enterprise/ra-rbac, as access control has now been reimplemented in react-admin. Use the same hook from react-admin instead:
-import { useCanAccess } from '@react-admin/ra-rbac';
-import { useRecordContext, DeleteButton } from 'react-admin';
+import { useCanAccess, useRecordContext, DeleteButton } from 'react-admin';

const DeleteUserButton = () => {
    const record = useRecordContext();
    const { isPending, canAccess } = useCanAccess({
        action: 'delete',
        resource: 'users',
        record,
    });
    if (isPending || !canAccess) return null;
    return <DeleteButton record={record} resource="users" />;
};
  • @ra-enterprise/ra-rbac no longer exports translations as they are included in react-admin, you can safely remove them:
import { mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
-import { raRbacLanguageEnglish, raRbacLanguageFrench } from '@react-admin/ra-rbac';

const myEnglishMessages = { /* ... */ };
const myFrenchMessages = { /* ... */ };

const i18nProvider = polyglotI18nProvider(locale =>
    locale === 'en'
-        ? mergeTranslations(englishMessages, raRbacLanguageEnglish, myEnglishMessages)
-        : mergeTranslations(frenchMessages, raRbacLanguageFrench, myFrenchMessages)
+        ? mergeTranslations(englishMessages, myEnglishMessages)
+        : mergeTranslations(frenchMessages, myFrenchMessages)
);
  • <AccordionForm>, <AccordionFormPanel>, <AccordionSection>, <LongForm>, <LongFormSection>, <WizardForm> and <WizardFormStep> are no longer exported by @react-admin/ra-enterprise. You can use the components exported by @react-admin/ra-form-layout and enable access control by passing them the enableAccessControl prop:
-import { AccordionForm } from '@react-admin/ra-enterprise';
+import { AccordionForm } from '@react-admin/ra-form-layout';

const authProvider = {
    // ...
    canAccess: async ({ action, resource, record }) =>
        canAccessWithPermissions({
            permissions: [
                // 'delete' is missing
                { action: ['list', 'edit'], resource: 'products' },
                { action: 'write', resource: 'products.reference' },
                { action: 'write', resource: 'products.width' },
                { action: 'write', resource: 'products.height' },
                // 'products.description' is missing
                { action: 'write', resource: 'products.thumbnail' },
                // 'products.image' is missing
                { action: 'write', resource: 'products.panel.description' },
                { action: 'write', resource: 'products.panel.images' },
                // 'products.panel.stock' is missing
            ],
            action,
            resource,
            record,
        }),
};

const ProductEdit = () => (
    <Edit>
-        <AccordionForm>
+        <AccordionForm enableAccessControl>
            <AccordionForm.Panel id="description" label="Description">
                <TextInput source="reference" />
                <TextInput source="width" />
                <TextInput source="height" />
                <TextInput source="description" />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="images" label="Images">
                <TextInput source="image" />
                <TextInput source="thumbnail" />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="stock" label="Stock">
                <TextInput source="stock" />
            </AccordionForm.Panel>
            // delete button not displayed
        </AccordionForm>
    </Edit>
);
  • <Toolbar> is no longer exported from @react-admin/ra-rbac. Import it from react-admin instead:
import * as React from "react";
-import { Create, SaveButton, useRedirect, useNotify } from 'react-admin';
-import { SimpleForm, Toolbar } from '@react-admin/ra-rbac';
+import { Create, SaveButton, Toolbar, useRedirect, useNotify } from 'react-admin';
+import { SimpleForm } from '@react-admin/ra-rbac';

const PostCreateToolbar = () => {
    const redirect = useRedirect();
    const notify = useNotify();
    return (
        <Toolbar>
            <SaveButton
                label="post.action.save_and_show"
            />
            <SaveButton
                label="post.action.save_and_add"
                mutationOptions={{
                    onSuccess: data => {
                        notify('ra.notification.created', {
                            type: 'info',
                            messageArgs: { smart_count: 1 },
                        });
                        redirect(false);
                    }}
                }
                type="button"
                variant="text"
            />
        </Toolbar>
    );
};

export const PostCreate = () => (
    <Create redirect="show">
        <SimpleForm toolbar={<PostCreateToolbar />}>
            ...
        </SimpleForm>
    </Create>
);

v5.0.2

2024-10-30

  • Fix <Edit redirect> console warning when redirect value is a function.

v5.0.1

2024-10-08

  • Backport from v4
    • Add support of hasCreate hasEdit hasShow props to the <Resource> component
    • Export matchTarget and matchWildcard utility functions
    • Export <DefaultUnauthorizedView>

v5.0.0

2024-07-25

  • Upgrade to react-admin v5
  • [TypeScript]: Enable strictNullChecks

v4.6.0

2024-06-05

  • Add support of hasCreate hasEdit hasShow props to the <Resource> component

v4.5.2

2024-05-14

  • Export matchTarget and matchWildcard utility functions
  • Export <DefaultUnauthorizedView>

v4.5.1

2024-04-04

  • Fix <Datagrid> shows selection checkboxes event with no bulk action buttons

v4.5.0

2024-02-22

  • Add support for permissions with an array of resources, e.g. [{ action: 'read', resource: ['products', 'categories'] }]

v4.4.3

2023-12-21

  • Export the TS type Record<string, Permissions> as RoleDefinitions (making it easier to use getPermissionsFromRoles)

v4.4.2

2023-10-31

  • Introduce <TabbedShowLayout>, to be used in conjuction with <Tab> to fix a bug where <Tab> only allows to hide tabs at the end of the tabs list
  • Export <Tab> as <TabbedShowLayout.Tab>
  • Fix <TabbedForm> to be used in conjuction with <FormTab> to fix a bug where <FormTab> only allows to hide tabs at the end of the tabs list
  • Export <FormTab> as <TabbedForm.Tab>

v4.4.1

2023-08-25

  • Fix <Resource> does not accept child Routes

v4.4.0

2023-07-20

  • canAccess does not require an action option anymore and support the wildcard (*) value for it too.
  • IfCanAccess takes an optional fallback prop that accept a ReactNode.

v4.3.0

2023-05-24

  • Upgraded to react-admin 4.10.6

v4.2.3

2023-03-28

  • Fix <IfCanAccess> so that it tries to get the record from the RecordContext.

v4.2.2

2023-01-25

  • Fix React warnings about unknown or invalid props

v4.2.1

2023-01-17

  • Fix <Resource> does not register its recordRepresentation prop

v4.2.0

2022-10-25

  • Fix Record-level Permissions for components that should already support them: SimpleShowLayout, Tab, SimpleForm and TabbedForm
  • Add Record-level Permissions support for <Edit> or <Show> views
  • Change the canAccess implementation to be more permissive about Record-level Permissions

(Minor) Breaking Changes

canAccess is more Permissive About Record-level Permissions

canAccess will only check Record-level Permissions if record is provided both in the permission and as a parameter. Otherwise it will only check Resource-level permissions.

const canAccess1 = canAccess({
    permissions: [{ action: 'show', resource: 'products', record: { id: 123 }}],
    action: 'show',
    resource: 'products',
    record: { id: 456 },
}); // returns false (unchanged)
const canAccess1 = canAccess({
    permissions: [{ action: 'show', resource: 'products', record: { id: 123 }}],
    action: 'show',
    resource: 'products',
    record: { id: 123 },
}); // returns true (unchanged)
const canAccess1 = canAccess({
    permissions: [{ action: 'show', resource: 'products', record: { id: 123 }}],
    action: 'show',
    resource: 'products',
    // record is omitted
}); // used to return false, now returns true
const canAccess1 = canAccess({
    permissions: [{ action: 'show', resource: 'products'}], // permission is resource-level
    action: 'show',
    resource: 'products',
    record: { id: 456 },
}); // returns true (unchanged)
const canAccess1 = canAccess({
    permissions: [{ action: "show", resource: "products", record: { id: 123 } }],
    action: "show",
    resource: "products",
    record: { id: 456 },
}); // returns false (unchanged)
const canAccess1 = canAccess({
    permissions: [{ action: "show", resource: "products", record: { id: 123 } }],
    action: "show",
    resource: "products",
    record: { id: 123 },
}); // returns true (unchanged)
const canAccess1 = canAccess({
    permissions: [{ action: "show", resource: "products", record: { id: 123 } }],
    action: "show",
    resource: "products",
    // record is omitted
}); // used to return false, now returns true
const canAccess1 = canAccess({
    permissions: [{ action: "show", resource: "products" }], // permission is resource-level
    action: "show",
    resource: "products",
    record: { id: 456 },
}); // returns true (unchanged)

ra-rbac now Provides Translation Strings

ra-rbac provides translations for English (raRbacLanguageEnglish) and French (raRbacLanguageFrench).

You should merge these translations with the other interface messages before passing them to your i18nProvider:

import { mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    raRbacLanguageEnglish,
    raRbacLanguageFrench,
} from '@react-admin/ra-rbac';

const i18nProvider = polyglotI18nProvider(locale =>
    locale === 'en'
        ? mergeTranslations(englishMessages, raRbacLanguageEnglish)
        : mergeTranslations(frenchMessages, raRbacLanguageFrench)
);
import { mergeTranslations } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raRbacLanguageEnglish, raRbacLanguageFrench } from "@react-admin/ra-rbac";

const i18nProvider = polyglotI18nProvider((locale) =>
    locale === "en"
        ? mergeTranslations(englishMessages, raRbacLanguageEnglish)
        : mergeTranslations(frenchMessages, raRbacLanguageFrench)
);

Tip: You should only need them if you use Record-level Permissions along with ra-rbac's <Edit> or <Show> view.

v4.1.1

2022-08-08

  • Fix <List> exporter includes non-authorized columns.

v4.1.0

2022-08-05

  • Add <Menu.Item> and the ability to define a <Menu> with custom items

v4.0.2

2022-07-20

  • (fix) Datagrid does not handle bulkActionButtons={false}

v4.0.1

2022-06-08

  • (fix) Update peer dependencies ranges (support React 18)

v4.0.0

2022-06-07

  • Upgrade to react-admin v4

Breaking changes

  • <WithPermissions> was renamed to <IfCanAccess> and no longer passes additional props to its child
-import { WithPermissions } from '@react-admin/ra-rbac';
+import { IfCanAccess } from '@react-admin/ra-rbac';
import { DeleteButton, EditButton, ShowButton } from 'react-admin';

const RecordToolbar = () => (
    <Toolbar>
-       <WithPermissions action="edit">
+       <IfCanAccess action="edit">
            <EditButton />
-       </WithPermissions>
+       </IfCanAccess>
-       <WithPermissions action="show">
+       <IfCanAccess action="show">
            <ShowButton />
-       </WithPermissions>
+       </IfCanAccess>
-       <WithPermissions action="delete">
+       <IfCanAccess action="delete">
            <DeleteButton />
-       </WithPermissions>
+       </IfCanAccess>
    </Toolbar>
);
  • Hooks now return a isLoading state instead of a loading state
-const { loading } = useAuthenticated();
+const { isLoading } = useAuthenticated();

-const { loading, canAccess } = useCanAccess();
+const { isLoading, canAccess } = useCanAccess();
  • authProvider.getRoles is no longer used (or required)
  • authProvider.getPermissions must return an array of permissions.

We simplified the authProvider so that it only needs the permissions. It is now your responsibility to eventually fetch the roles definitions and to merge the users specific permissions with those defined for their roles. However, we provide a function to ease this process:

+ import { getPermissionsFromRoles } from '@react-admin/ra-rbac';
const authProvider = {
    // Other methods omitted for brevity
    getPermissions: () => {
        Promise.resolve(
-            {
-                permissions: [
-                    {
-                        action: ['read', 'write'],
-                        resource: 'users',
-                        record: { id: '123' },
-                    },
-                ],
-                roles: ['reader'],
-            },
+            getPermissionsFromRoles({
+                roleDefinitions: {
+                    admin: [{ action: '*', resource: '*' }],
+                    reader: [{ action: 'read', resource: '*' }],
+                },
+                userPermissions: [
+                    {
+                        action: ['read', 'write'],
+                        resource: 'users',
+                        record: { id: '123' },
+                    }
+                ],
+                userRoles: ['reader'],
+            })
+        );
    },
-    getRoles: () =>
-        Promise.resolve({
-            admin: [{ action: '*', resource: '*' }],
-            reader: [{ action: 'read', resource: '*' }],
-        }),
};
  • The usePermissions hook has been removed. Use the react-admin version instead.
-import { usePermissions } from '@react-admin/ra-rbac';
+import { usePermissions } from 'react-admin';

v1.0.2

2021-11-19

  • (fix) WithPermissions should not override children props.

v1.0.1

2021-10-27

  • (fix) WithPermissions should pass record and resource to its children.

v1.0.0

2021-07-31

  • First release