ra-rbac
Role-Based Access Control for React-admin apps. This module extends the authProvider
to manage roles and fine-grained permissions, and adds replacement for many react-admin components that use these permissions.
Test it live in the Enterprise Edition Storybook.
You can define permissions for pages, fields, buttons, etc. Roles and permissions are managed by the authProvider
, which means you can use any data source you want (including an ActiveDirectory server).
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.
Security Considerations
The purpose of this package is to provide hooks and components that will make it easier to shape the UI of an Admin app depending heavily on role/permissions.
However, please be aware that this package does by no means add a security layer to your application. React-admin being a front-end framework, it cannot prevent a malicious user with a little bit of knowledge to temper with the client-side code, which could result in permissions checks bypass.
Always keep in mind that the security of your application, and your data, is the responsibility of your back-end.
Vocabulary
Permission
A permission is an object that represents 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".
Ra-rbac defines its own actions that you can use with ra-rbac components, but you can also define your own actions, and implement them in your own components using useCanAccess
, canAccess
or <IfCanAccess>
.
Ra-rbac's built-in actions operate at different levels:
- Page: controls visibility of a page like the Edit page
- Field: controls visibility of a specific field, for example in a form
- Action: controls permission to perform global actions, like exporting data
Here are all the actions supported by ra-rbac:
Action | Level | Description | Used In |
---|---|---|---|
list |
Page | Allow to access the List page | <Resource> , <Menu> |
show |
Page | Allow to access the Show page | <Resource> , <Datagrid> , <Edit> , <Show> |
create |
Page | Allow to access the Create page | <Resource> , <List> |
edit |
Page | Allow to access the Edit page | <Resource> , <Datagrid> , <Edit> , <Show> |
export |
Action | Allow to export data | <List> |
delete |
Action | Allow to delete data | <Datagrid> , <SimpleForm> , <TabbedForm> |
clone |
Action | Allow to clone a record | <Edit> |
read |
Field | Allow to view a field (or a tab) | <Datagrid> , <SimpleShowLayout> , <TabbedShowLayout> |
write |
Field | Allow to edit a field (or a tab) | <SimpleForm> , <TabbedForm> |
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.
Concepts
Pessimistic Strategy
React-admin treats permissions in an optimistic way: While it fetches permissions from the authProvider, react-admin renders all components. If the authProvider returns a limited set of permissions, users may briefly see content they don't have access to.
Ra-rbac takes the opposite strategy: 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 ra-rbac is sure that the user has the right permissions that it renders the content.
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 user of id 123
const perm3 = { action: 'edit', resource: 'users', record: { id: '123' } };
// can only edit field 'username' for user of id 123
const perm4 = { action: 'write', resource: 'users.username', record: { id: '123' } };
// can access only comments by user of id 123
const perm5 = { action: '*', resource: 'comments', record: { user_id: '123' } };
Of course, 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
Ra-rbac builds up on react-admin's authProvider
API. It precises the return format of the getPermissions()
method: it must be a promise for an array of permissions.
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{
action: ['read', 'write'],
resource: 'users',
record: { id: '123' },
},
{ action: 'read', resource: '*' },
]),
};
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{
action: ["read", "write"],
resource: "users",
record: { id: "123" },
},
{ action: "read", resource: "*" },
]),
};
For every restricted resource, ra-rbac calls authProvider.getPermissions()
to get the user permissions.
It is your responsibility to fetch the roles definitions and to merge them with the users permissions based on their roles. As this is a very common task, ra-rbac provides the getPermissionsFromRoles
function to ease this process.
Internationalization
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)
);
getPermissionsFromRoles
Based on a role definition, a list of roles, and a list of user permissions, this function returns all the permissions that the user has.
This function takes an object as argument with the following fields:
roleDefinitions
: a static object containing the role definitions for each roleuserPermissions
(optional): an array of permissions for the current useruserRoles
(optional): an array of roles (admin, reader...) for the current user
The getPermissionsFromRoles
function merges the userPermissions
with the permissions defined in roleDefinitions
for the current user's roles (userRoles
).
import { getPermissionsFromRoles } from '@react-admin/ra-rbac';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve(
getPermissionsFromRoles({
// static role definitions
roleDefinitions: {
admin: [{ action: '*', resource: '*' }],
reader: [{ action: 'read', resource: '*' }],
},
// permissions for the current user
userPermissions: [
{
action: ['read', 'write'],
resource: 'users',
record: { id: '123' },
},
],
// roles of the current user
userRoles: ['reader'],
})
),
};
In practice, the roles and permissions are usually returned upon login rather than in the authProvider
code. The authProvider stores the roles and permissions in memory, or in localStorage. In that case, authProvider.getPermissions()
reads the role definitions, roles and permissions from localStorage.
Here is an example authProvider
implementation following this pattern:
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);
},
};
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);
},
};
Tip: If you have to rely on the server for roles and permissions, check out the Performance section below.
useAuthenticated
A pessimistic version of react-admin's useAuthenticated
hook, which returns false
until the authProvider.checkAuth()
promise is resolved.
import { useAuthenticated } from '@react-admin/ra-rbac';
const SecretData = () => {
const { authenticated } = useAuthenticated();
return authenticated ? null : <span>For your eyes only</span>;
};
import { useAuthenticated } from "@react-admin/ra-rbac";
const SecretData = () => {
const { authenticated } = useAuthenticated();
return authenticated ? null : <span>For your eyes only</span>;
};
useCanAccess
This hook calls authProvider.getPermissions()
, then checks whether the requested action and resource are allowed for the current user.
useCanAccess
takes an object { action, resource, record }
as argument. It returns an object describing the state of the RBAC request. As calls to the authProvider
are asynchronous, the hook returns a isPending
state in addition to the canAccess
key.
import { useCanAccess } from '@react-admin/ra-rbac';
import { 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" />;
};
import { useCanAccess } from "@react-admin/ra-rbac";
import { 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" />;
};
When checking if a user can access a resource, ra-rbac grabs the permissions. If at least one of these permissions allows him to access the resource, the user is granted access. Otherwise, the user is denied.
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{
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', 'edit', 'delete'],
resource: 'tasks',
},
{
action: ['read', 'write'],
resource: 'sales',
record: { id: '123' },
},
]),
};
const { canAccess: canUseCompanyResource } = useCanAccess({
resource: 'companies',
}); // canUseCompanyResource is true
const { canAccess: canUseCompanyResourceFromWildcard } = useCanAccess({
resource: 'companies',
action: '*',
}); // canUseCompanyResourceFromWildcard is true
const { canAccess: canReadCompanies } = useCanAccess({
action: 'read',
resource: 'companies',
}); // canReadCompanies is true
const { canAccess: canCreatePeople } = useCanAccess({
action: 'create',
resource: 'people',
}); // canCreatePeople is true
const { canAccess: canExportPeople } = useCanAccess({
action: 'export',
resource: 'people',
}); // canExportPeople is false
const { canAccess: canEditDeals } = useCanAccess({
action: 'edit',
resource: 'deals',
}); // canEditDeals is true
const { canAccess: canDeleteComments } = useCanAccess({
action: 'delete',
resource: 'tasks',
}); // canDeleteComments is true
const { canAccess: canReadSales } = useCanAccess({
action: 'read',
resource: 'sales',
}); // canReadSales is false
const { canAccess: canReadSelfSales } = useCanAccess(
{ action: 'read', resource: 'sales' },
{ id: '123' }
); // canReadSelfSales is true
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{
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", "edit", "delete"],
resource: "tasks",
},
{
action: ["read", "write"],
resource: "sales",
record: { id: "123" },
},
]),
};
const { canAccess: canUseCompanyResource } = useCanAccess({
resource: "companies",
}); // canUseCompanyResource is true
const { canAccess: canUseCompanyResourceFromWildcard } = useCanAccess({
resource: "companies",
action: "*",
}); // canUseCompanyResourceFromWildcard is true
const { canAccess: canReadCompanies } = useCanAccess({
action: "read",
resource: "companies",
}); // canReadCompanies is true
const { canAccess: canCreatePeople } = useCanAccess({
action: "create",
resource: "people",
}); // canCreatePeople is true
const { canAccess: canExportPeople } = useCanAccess({
action: "export",
resource: "people",
}); // canExportPeople is false
const { canAccess: canEditDeals } = useCanAccess({
action: "edit",
resource: "deals",
}); // canEditDeals is true
const { canAccess: canDeleteComments } = useCanAccess({
action: "delete",
resource: "tasks",
}); // canDeleteComments is true
const { canAccess: canReadSales } = useCanAccess({
action: "read",
resource: "sales",
}); // canReadSales is false
const { canAccess: canReadSelfSales } = useCanAccess({ action: "read", resource: "sales" }, { id: "123" }); // canReadSelfSales is true
canAccess
This helper function can check if the current permissions allow the user to execute an action on a resource (and optionally a record). It requires the permissions
array, so it must be used in conjunction with usePermissions
.
canAccess
expects an object { permissions, resource, action, record }
as parameter, and returns a boolean.
canAccess({
permissions: [
{ action: 'read', resource: 'user' },
{ action: ['read', 'edit', 'create', 'delete'], resource: 'posts' },
],
action: 'edit',
resource: 'posts',
}); // true
canAccess
is very useful to hide restricted elements in the admin, e.g. to hide columns in a datagrid:
import { List, Datagrid, TextField } from 'react-admin';
import { canAccess } from '@react-admin/ra-rbac';
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
{ action: 'list', resource: 'products' },
{ action: 'read', resource: 'products.price' },
]),
};
const ProductList = () => {
const { permissions } = usePermissions();
return (
<List>
<Datagrid>
<TextField source="id" />
<TextField source="reference" />
<TextField source="width" />
<TextField source="height" />
{canAccess({
permissions,
action: 'read',
resource: 'products.price',
}) && <TextField source="price" />}
{/* this column will not render */}
{canAccess({
permissions,
action: 'read',
resource: 'products.stock',
}) && <TextField source="stock" />}
</Datagrid>
</List>
);
};
import { List, Datagrid, TextField } from "react-admin";
import { canAccess } from "@react-admin/ra-rbac";
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
{ action: "list", resource: "products" },
{ action: "read", resource: "products.price" },
]),
};
const ProductList = () => {
const { permissions } = usePermissions();
return (
<List>
<Datagrid>
<TextField source="id" />
<TextField source="reference" />
<TextField source="width" />
<TextField source="height" />
{canAccess({
permissions,
action: "read",
resource: "products.price",
}) && <TextField source="price" />}
{/* this column will not render */}
{canAccess({
permissions,
action: "read",
resource: "products.stock",
}) && <TextField source="stock" />}
</Datagrid>
</List>
);
};
Tip: Ra-rbac actually proposes a <Datagrid>
component that hides columns depending on permissions. Check out the <Datagrid>
component below.
You don't have to provide an action
if you just want to know whether users can access any of the resources. For instance, here's how you may display different components depending on resources access rights in the dashboard:
import { Admin } from 'react-admin';
import { canAccess } from '@react-admin/ra-rbac';
import { dataProvider } from './dataProvider';
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
{ action: 'list', resource: 'products' },
{ action: 'edit', resource: 'categories' },
]),
};
const AccessDashboard = () => {
const { permissions } = usePermissions();
return (
<>
{canAccess({
permissions,
resource: 'orders',
}) ? (
<>List of last orders...</> // no access to this component
) : null}
{canAccess({
permissions,
resource: 'products',
}) ? (
<>List of last products...</>
) : null}
{canAccess({
permissions,
resource: 'categories',
}) ? (
<>List of last categories...</>
) : null}
</>
);
};
export const MyApp = () => (
<Admin authProvider={authProvider} dataProvider={dataProvider} dashboard={AccessDashboard}>
{/*...*/}
</Admin>
);
import { Admin } from "react-admin";
import { canAccess } from "@react-admin/ra-rbac";
import { dataProvider } from "./dataProvider";
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
{ action: "list", resource: "products" },
{ action: "edit", resource: "categories" },
]),
};
const AccessDashboard = () => {
const { permissions } = usePermissions();
return (
<>
{canAccess({
permissions,
resource: "orders",
}) ? (
<>List of last orders...</> // no access to this component
) : null}
{canAccess({
permissions,
resource: "products",
}) ? (
<>List of last products...</>
) : null}
{canAccess({
permissions,
resource: "categories",
}) ? (
<>List of last categories...</>
) : null}
</>
);
};
export const MyApp = () => (
<Admin authProvider={authProvider} dataProvider={dataProvider} dashboard={AccessDashboard}>
{/*...*/}
</Admin>
);
In this example, users will see the list of last products and the list of last categories but they won't be able to see the list of last orders.
<IfCanAccess>
This component relies on the authProvider
to render its child only if the user has the right permissions. It accepts the following props:
action
(string
, required): the action to check, e.g. 'read', 'list', 'export', 'delete', etc.resource
(string
, optional): the resource to check, e.g. 'users', 'comments', 'posts', etc. Falls back to the current resource context if absent.record
(object
, optional): the record to check. If passed, the child only renders if the user has permissions for that record, e.g.{ id: 123, firstName: "John", lastName: "Doe" }
fallback
(ReactNode
, optional): The element to render when the user does not have the permission. Defaults tonull
.
Additional props are passed down to the child element.
import { IfCanAccess } from '@react-admin/ra-rbac';
import { DeleteButton, EditButton, ShowButton } from 'react-admin';
const RecordToolbar = () => (
<Toolbar>
<IfCanAccess action="edit">
<EditButton />
</IfCanAccess>
<IfCanAccess action="show">
<ShowButton />
</IfCanAccess>
<IfCanAccess action="delete">
<DeleteButton />
</IfCanAccess>
</Toolbar>
);
import { IfCanAccess } from "@react-admin/ra-rbac";
import { DeleteButton, EditButton, ShowButton } from "react-admin";
const RecordToolbar = () => (
<Toolbar>
<IfCanAccess action="edit">
<EditButton />
</IfCanAccess>
<IfCanAccess action="show">
<ShowButton />
</IfCanAccess>
<IfCanAccess action="delete">
<DeleteButton />
</IfCanAccess>
</Toolbar>
);
<Resource>
To restrict access to Create, Edit, List and Show views for your resources, use the <Resource>
component from ra-rbac rather than the one from react-admin:
import { Admin } from 'react-admin';
import { Resource } from '@react-admin/ra-rbac';
import { UserList, UserEdit, UserShow, UserCreate } from './users';
import {
CommentList,
CommentEdit,
CommentCreate,
CommentShow,
} from './comments';
import dataProvider from './dataProvider';
import authProvider from './authProvider';
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource
name="users"
list={UserList}
edit={UserEdit}
show={UserShow}
create={UserCreate}
/>
<Resource
name="comments"
list={CommentList}
edit={CommentEdit}
create={CommentCreate}
show={CommentShow}
/>
</Admin>
);
import { Admin } from "react-admin";
import { Resource } from "@react-admin/ra-rbac";
import { UserList, UserEdit, UserShow, UserCreate } from "./users";
import { CommentList, CommentEdit, CommentCreate, CommentShow } from "./comments";
import dataProvider from "./dataProvider";
import authProvider from "./authProvider";
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="users" list={UserList} edit={UserEdit} show={UserShow} create={UserCreate} />
<Resource name="comments" list={CommentList} edit={CommentEdit} create={CommentCreate} show={CommentShow} />
</Admin>
);
Ra-rbac's <Resource>
relies on the following actions:
list
to enable the list viewshow
to enable the show viewcreate
to enable the create viewedit
to enable the edit view
<Menu>
A replacement for react-admin's <Menu>
component, which only displays the menu items that the current user has access to (using the list
action).
Pass this menu to a <Layout>
, and pass that layout to the <Admin>
component to use it.
import { Admin, Resource, ListGuesser, Layout, LayoutProps } from 'react-admin';
import { Menu } from '@react-admin/ra-rbac';
import * as posts from './posts';
import * as comments from './comments';
import * as users from './users';
import dataProvider from './dataProvider';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ action: '*', resource: 'posts' },
{ action: '*', resource: 'comments' },
]),
};
const CustomLayout = (props: LayoutProps) => <Layout {...props} menu={Menu} />;
const App = () => (
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
layout={CustomLayout}
>
<Resource name="posts" {...posts} />
<Resource name="comments" {...comments} />
{/* the user won't see the Users menu */}
<Resource name="users" {...users} />
</Admin>
);
import { Admin, Resource, Layout } from "react-admin";
import { Menu } from "@react-admin/ra-rbac";
import * as posts from "./posts";
import * as comments from "./comments";
import * as users from "./users";
import dataProvider from "./dataProvider";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ action: "*", resource: "posts" },
{ action: "*", resource: "comments" },
]),
};
const CustomLayout = (props) => <Layout {...props} menu={Menu} />;
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider} layout={CustomLayout}>
<Resource name="posts" {...posts} />
<Resource name="comments" {...comments} />
{/* the user won't see the Users menu */}
<Resource name="users" {...users} />
</Admin>
);
If you want to build a menu with custom navigation links, create a custom component and use <Menu.Item>
for each menu item. This component behaves like react-admin's <Menu.Item>
, except that it accepts an action
and a resource
prop. If given, the menu item will only render if the user has the right permissions.
import { Menu } from '@react-admin/ra-rbac';
export const MyMenu = () => (
<Menu>
{/* these menu items will only render for users with the right permissions */}
<Menu.Item
to="/products"
resource="products"
action="list"
primaryText="Products"
/>
<Menu.Item
to="/categories"
resource="categories"
action="list"
primaryText="Categories"
/>
<Menu.Item
to="/orders"
resource="orders"
action="list"
primaryText="Orders"
/>
{/* 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>
{/* these menu items will only render for users with the right permissions */}
<Menu.Item to="/products" resource="products" action="list" primaryText="Products" />
<Menu.Item to="/categories" resource="categories" action="list" primaryText="Categories" />
<Menu.Item to="/orders" resource="orders" action="list" primaryText="Orders" />
{/* this menu item will render for all users */}
<Menu.Item to="/preferences" primaryText="Preferences" />
</Menu>
);
List Components
Ra-rbac provides replacements for List page components leveraging roles and permissions.
<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}` }.
import { List } from '@react-admin/ra-rbac';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
export const PostList = () => <List>// ...</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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
export const PostList = () => <List> // ...</List>;
// ...</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'll have to filter the authorized columns yourself. Here is the default exporter of ra-rbac's <List>
as a base for that custom exporter:
const defaultExporter = records => {
const recordsWithAuthorizedColumns = records.map(record =>
Object.keys(record)
.filter(key =>
canAccess({
permissions,
action: 'read',
resource: `${resource}.${key}`,
})
)
.reduce((obj, key) => ({ ...obj, [key]: record[key] }), {})
);
jsonExport(recordsWithAuthorizedColumns, (err, csv) =>
downloadCSV(csv, resource)
);
};
const defaultExporter = (records) => {
const recordsWithAuthorizedColumns = records.map((record) =>
Object.keys(record)
.filter((key) =>
canAccess({
permissions,
action: "read",
resource: `${resource}.${key}`,
})
)
.reduce((obj, key) => ({ ...obj, [key]: record[key] }), {})
);
jsonExport(recordsWithAuthorizedColumns, (err, csv) => downloadCSV(csv, resource));
};
Tip: This <List>
component relies on the <ListActions>
component below.
<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}` }.
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 { List, Datagrid } from '@react-admin/ra-rbac';
import {
ImageField,
TextField,
ReferenceField,
NumberField,
} from 'react-admin';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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' },
]),
};
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 { List, Datagrid } from "@react-admin/ra-rbac";
import { ImageField, TextField, ReferenceField, NumberField } from "react-admin";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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" },
]),
};
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.*" }.
<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>;
Detail Components
Ra-rbac provides replacements for Edit, Create and Show page components leveraging roles and permissions.
<Edit>
Replacement for react-admin's <Edit>
that adds RBAC control to actions
Users must have the 'edit' permission on the resource and record to see the Edit view. Users must have the 'show' permission on the resource and record to see the ShowButton. Users must have the 'clone' permission on the resource and record to see the CloneButton.
import { EditProps } from 'react-admin';
import { Edit } from '@react-admin/ra-rbac';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ action: ['list', 'edit', 'clone'], resource: 'products' },
]),
};
export const PostEdit = () => <Edit>// ...</Edit>;
// user will see the clone button but not the show button
import { Edit } from "@react-admin/ra-rbac";
const authProvider = {
// ...
getPermissions: () => Promise.resolve([{ action: ["list", "edit", "clone"], resource: "products" }]),
};
export const PostEdit = () => <Edit> // ...</Edit>;
// ...</Edit>;
// user will see the clone button but not the show button
Tip: You can customize the component that will be displayed when the access to the Edit view is unauthorized with the unauthorized
prop:
import { Edit, SimpleForm, TextInput } from '@react-admin/ra-rbac';
const CustomUnauthorizedView = () => <p>Custom Unauthorized View</p>;
const ProductEditCustomUnauthorized = () => (
<Edit unauthorized={<CustomUnauthorizedView />}>
<SimpleForm>
<TextInput source="reference" />
</SimpleForm>
</Edit>
);
import { Edit, SimpleForm, TextInput } from "@react-admin/ra-rbac";
const CustomUnauthorizedView = () => <p>Custom Unauthorized View</p>;
const ProductEditCustomUnauthorized = () => (
<Edit unauthorized={<CustomUnauthorizedView />}>
<SimpleForm>
<TextInput source="reference" />
</SimpleForm>
</Edit>
);
<Show>
Replacement for react-admin's <Show>
that adds RBAC control to actions
Users must have the 'show' permission on the resource and record to see the Show view. Users must have the 'edit' permission on the resource and record to see the EditButton.
import { ShowProps } from 'react-admin';
import { Show } from '@react-admin/ra-rbac';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ action: ['list', 'show', 'edit'], resource: 'products' },
]),
};
export const PostShow = () => <Show>// ...</Show>;
// user will see the edit action on top of the Show view
import { Show } from "@react-admin/ra-rbac";
const authProvider = {
// ...
getPermissions: () => Promise.resolve([{ action: ["list", "show", "edit"], resource: "products" }]),
};
export const PostShow = () => <Show> // ...</Show>;
// ...</Show>;
// user will see the edit action on top of the Show view
Tip: You can customize the component that will be displayed when the access to the Show view is unauthorized with the unauthorized
prop:
import { Show, SimpleShowLayout, TextField } from '@react-admin/ra-rbac';
const CustomUnauthorizedView = () => <p>Custom Unauthorized View</p>;
const ProductShowCustomUnauthorized = () => (
<Show unauthorized={<CustomUnauthorizedView />}>
<SimpleShowLayout>
<TextField source="reference" />
</SimpleShowLayout>
</Show>
);
import { Show, SimpleShowLayout, TextField } from "@react-admin/ra-rbac";
const CustomUnauthorizedView = () => <p>Custom Unauthorized View</p>;
const ProductShowCustomUnauthorized = () => (
<Show unauthorized={<CustomUnauthorizedView />}>
<SimpleShowLayout>
<TextField source="reference" />
</SimpleShowLayout>
</Show>
);
<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}` }
import { ShowProps } from 'react-admin';
import { SimpleShowLayout } from '@react-admin/ra-rbac';
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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' },
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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" },
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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' },
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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" },
]),
};
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>
);
Form Components
Ra-rbac provides replacements for Form components leveraging roles and permissions.
<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 = {
// ...
getPermissions: () =>
Promise.resolve([
// '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
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
// '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
]),
};
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 = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
// 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' },
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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' },
]),
};
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 = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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" },
]),
};
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>
);
<AccordionForm>
Alternative to react-admin's <AccordionForm>
that adds RBAC control to the delete button.
This component is provided by the @react-admin/ra-enterprise
package.
import { AccordionForm } from '@react-admin/ra-enterprise';
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>Promise.resolve([
// '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
]),
};
const ProductEdit = () => (
<Edit>
<AccordionForm>
<AccordionForm.Panel label="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
<TextInput source="description" />
</AccordionForm.Panel>
<AccordionForm.Panel label="images" label="Images">
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionForm.Panel>
<AccordionForm.Panel label="stock" label="Stock">
<TextInput source="stock" />
</AccordionForm.Panel>
// delete button not displayed
</AccordionForm>
</Edit>
);
import { AccordionForm } from "@react-admin/ra-enterprise";
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
// '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
]),
};
const ProductEdit = () => (
<Edit>
<AccordionForm>
<AccordionForm.Panel label="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
<TextInput source="description" />
</AccordionForm.Panel>
<AccordionForm.Panel label="images" label="Images">
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionForm.Panel>
<AccordionForm.Panel label="stock" label="Stock">
<TextInput source="stock" />
</AccordionForm.Panel>
// delete button not displayed
</AccordionForm>
</Edit>
);
<AccordionFormPanel>
Replacement for the default <AccordionFormPanel>
that only renders a section if the user has the right permissions.
Add a name
prop to the AccordionFormPanel
to define the sub-resource that the user needs to have the right permissions for.
<AccordionFormPanel>
also only renders the child inputs for which the user has the 'write' permissions.
import { Edit, TextInput } from 'react-admin';
import { AccordionForm } from '@react-admin/ra-form-layout';
import { AccordionFormPanel } from '@react-admin/ra-enterprise';
const authProvider = {
// ...
getPermissions: () => Promise.resolve([
{ 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
]),
};
const ProductEdit = () => (
<Edit>
<AccordionForm>
<AccordionFormPanel label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</AccordionFormPanel>
<AccordionFormPanel label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionFormPanel>
// not displayed
<AccordionFormPanel label="Stock" name="stock">
<TextInput source="stock" />
</AccordionFormPanel>
</AccordionForm>
</Edit>
);
import { Edit, TextInput } from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";
import { AccordionFormPanel } from "@react-admin/ra-enterprise";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
const ProductEdit = () => (
<Edit>
<AccordionForm>
<AccordionFormPanel label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</AccordionFormPanel>
<AccordionFormPanel label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionFormPanel>
// not displayed
<AccordionFormPanel label="Stock" name="stock">
<TextInput source="stock" />
</AccordionFormPanel>
</AccordionForm>
</Edit>
);
<AccordionSection>
Replacement for the default <AccordionSection>
that only renders a section if the user has the right permissions.
Add a name
prop to the AccordionSection
to define the sub-resource that the user needs to have the right permissions for.
<AccordionSection>
also only renders the child inputs for which the user has the 'write' permissions.
This component is provided by the @react-admin/ra-enterprise
package.
import { Edit, SimpleForm, TextInput } from 'react-admin';
import { AccordionSection } from '@react-admin/ra-enterprise';
const authProvider = {
// ...
getPermissions: () => Promise.resolve([
{ 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.section.description' },
{ action: 'write', resource: 'products.section.images' },
// 'products.section.stock' is missing
]),
};
const ProductEdit = () => (
<Edit>
<SimpleForm>
<AccordionSection label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</AccordionSection>
<AccordionSection label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionSection>
// not displayed
<AccordionSection label="Stock" name="stock">
<TextInput source="stock" />
</AccordionSection>
</SimpleForm>
</Edit>
);
import { Edit, SimpleForm, TextInput } from "react-admin";
import { AccordionSection } from "@react-admin/ra-enterprise";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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.section.description" },
{ action: "write", resource: "products.section.images" },
// 'products.section.stock' is missing
]),
};
const ProductEdit = () => (
<Edit>
<SimpleForm>
<AccordionSection label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</AccordionSection>
<AccordionSection label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</AccordionSection>
// not displayed
<AccordionSection label="Stock" name="stock">
<TextInput source="stock" />
</AccordionSection>
</SimpleForm>
</Edit>
);
<LongForm>
Alternative to react-admin's <LongForm>
that adds RBAC control to the delete button and hides sections users don't have access to.
This component is provided by the @react-admin/ra-enterprise
package.
Use in conjunction with ra-enterprise's <LongForm.Section>
to render inputs based on permissions.
import { LongForm } from '@react-admin/ra-enterprise';
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>Promise.resolve([
// '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.Section.description' },
{ action: 'write', resource: 'products.Section.images' },
// 'products.Section.stock' is missing
]),
};
const ProductEdit = () => (
<Edit>
<LongForm>
<LongForm.Section name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
<TextInput source="description" />
</LongForm.Section>
<LongForm.Section name="images" label="Images">
<TextInput source="image" />
<TextInput source="thumbnail" />
</LongForm.Section>
<LongForm.Section name="stock" label="Stock">
<TextInput source="stock" />
</LongForm.Section>
// delete button not displayed
</LongForm>
</Edit>
);
import { LongForm } from "@react-admin/ra-enterprise";
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
// '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.Section.description" },
{ action: "write", resource: "products.Section.images" },
// 'products.Section.stock' is missing
]),
};
const ProductEdit = () => (
<Edit>
<LongForm>
<LongForm.Section name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
<TextInput source="description" />
</LongForm.Section>
<LongForm.Section name="images" label="Images">
<TextInput source="image" />
<TextInput source="thumbnail" />
</LongForm.Section>
<LongForm.Section name="stock" label="Stock">
<TextInput source="stock" />
</LongForm.Section>
// delete button not displayed
</LongForm>
</Edit>
);
<LongFormSection>
Replacement for the default <LongFormSection>
that only renders a section if the user has the right permissions.
Add a name
prop to the LongFormSection
to define the sub-resource that the user needs to have the right permissions for.
<LongFormSection>
also only renders the child inputs for which the user has the 'write' permissions.
This component is provided by the @react-admin/ra-enterprise
package.
import { LongForm, LongFormSection } from '@react-admin/ra-enterprise';
const authProvider = {
// ...
getPermissions: () => Promise.resolve([
{ 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
]),
};
const ProductEdit = () => (
<Edit>
<LongForm>
<LongFormSection name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</LongFormSection>
<LongFormSection name="images" label="Images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</LongFormSection>
// not displayed
<LongFormSection name="stock" label="Stock">
<TextInput source="stock" />
</LongFormSection>
</LongForm>
</Edit>
);
import { LongForm, LongFormSection } from "@react-admin/ra-enterprise";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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
]),
};
const ProductEdit = () => (
<Edit>
<LongForm>
<LongFormSection name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</LongFormSection>
<LongFormSection name="images" label="Images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</LongFormSection>
// not displayed
<LongFormSection name="stock" label="Stock">
<TextInput source="stock" />
</LongFormSection>
</LongForm>
</Edit>
);
<WizardForm>
Alternative to react-admin's <WizardForm>
that adds RBAC control to hide steps users don't have access to.
Use in conjunction with ra-enterprise's <WizardForm.Step>
to render inputs based on permissions.
This component is provided by the @react-admin/ra-enterprise
package.
import { WizardForm } from '@react-admin/ra-enterprise';
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>Promise.resolve([
// '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.step.description' },
{ action: 'write', resource: 'products.step.images' },
// 'products.step.stock' is missing
]),
};
const ProductCreate = () => (
<Create>
<WizardForm>
<WizardForm.Step name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
{/* Won't be displayed */}
<TextInput source="description" />
</WizardForm.Step>
<WizardForm.Step name="images" label="Images">
{/* Won't be displayed */}
<TextInput source="image" />
<TextInput source="thumbnail" />
</WizardForm.Step>
{/* Won't be displayed */}
<WizardForm.Step name="stock" label="Stock">
<TextInput source="stock" />
</WizardForm.Step>
</WizardForm>
</Create>
);
import { WizardForm } from "@react-admin/ra-enterprise";
const authProvider = {
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
Promise.resolve([
// '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.step.description" },
{ action: "write", resource: "products.step.images" },
// 'products.step.stock' is missing
]),
};
const ProductCreate = () => (
<Create>
<WizardForm>
<WizardForm.Step name="description" label="Description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
{/* Won't be displayed */}
<TextInput source="description" />
</WizardForm.Step>
<WizardForm.Step name="images" label="Images">
{/* Won't be displayed */}
<TextInput source="image" />
<TextInput source="thumbnail" />
</WizardForm.Step>
{/* Won't be displayed */}
<WizardForm.Step name="stock" label="Stock">
<TextInput source="stock" />
</WizardForm.Step>
</WizardForm>
</Create>
);
<WizardFormStep>
Replacement for the default <WizardFormStep>
that only renders a step if the user has the right permissions.
Use it with <WizardForm>
from @react-admin/ra-enterprise
to only display the steps the user has access to in the stepper.
This component is provided by the @react-admin/ra-enterprise
package.
Add a name
prop to the WizardFormStep to define the sub-resource that the user needs to have the right permissions for.
<WizardFormStep>
also only renders the child inputs for which the user has the 'write' permissions.
import { Edit, TextInput } from 'react-admin';
import { WizardForm, WizardFormStep } from '@react-admin/ra-enterprise';
const authProvider = {
// ...
getPermissions: () => Promise.resolve([
{ 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.step.description' },
{ action: 'write', resource: 'products.step.images' },
// 'products.step.stock' is missing
]),
};
const ProductCreate = () => (
<Create>
<WizardForm>
<WizardForm.Step label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</WizardForm.Step>
<WizardForm.Step label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</WizardForm.Step>
// not displayed
<WizardForm.Step label="Stock" name="stock">
<TextInput source="stock" />
</WizardForm.Step>
</WizardForm>
</Create>
);
import { TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-enterprise";
const authProvider = {
// ...
getPermissions: () =>
Promise.resolve([
{ 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.step.description" },
{ action: "write", resource: "products.step.images" },
// 'products.step.stock' is missing
]),
};
const ProductCreate = () => (
<Create>
<WizardForm>
<WizardForm.Step label="Description" name="description">
<TextInput source="reference" />
<TextInput source="width" />
<TextInput source="height" />
// not displayed
<TextInput source="description" />
</WizardForm.Step>
<WizardForm.Step label="Images" name="images">
// not displayed
<TextInput source="image" />
<TextInput source="thumbnail" />
</WizardForm.Step>
// not displayed
<WizardForm.Step label="Stock" name="stock">
<TextInput source="stock" />
</WizardForm.Step>
</WizardForm>
</Create>
);
Performance
authProvider.getPermissions()
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 { 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));
});
},
// ...
getPermissions: async () => {
if (Date.now() > rolesExpiresAt) {
await getRoles();
}
if (Date.now() > permissionsExpiresAt) {
await getPermissions();
}
return getPermissionsFromRoles({
roleDefinitions,
userPermissions: permissions,
userRoles: localStorage.getItem('userRoles'),
});
},
};
import { 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));
});
},
// ...
getPermissions: async () => {
if (Date.now() > rolesExpiresAt) {
await getRoles();
}
if (Date.now() > permissionsExpiresAt) {
await getPermissions();
}
return getPermissionsFromRoles({
roleDefinitions,
userPermissions: permissions,
userRoles: localStorage.getItem("userRoles"),
});
},
};
Showing An Access Denied Message Instead Of A Not Found Page
ra-rbac
shows a Not Found page when users try to access a page they don't have the permissions for. It is considered good security practice not to disclose to a potentially malicious user that a page exists if they are not allowed to see it.
However, should you prefer to show an Access Denied screen in those cases, you can do so by using the Resource
component from react-admin
instead of the one from ra-rbac
and leveraging the IfCanAccess
component in your views:
// In src/App.tsx
import { Admin, Resource } from 'react-admin';
import { dataProvider } from './dataProvider';
import { authProvider } from './authProvider';
import posts from './posts';
export const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" {...posts} />
</Admin>
);
// 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 { AccessDenied } from '../AccessDenied';
export const PostCreate = () => (
<IfCanAccess action="create" fallback={<AccessDenied />}>
<Create>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</Create>
</IfCanAccess>
);
// In src/App.tsx
import { Admin, Resource } from "react-admin";
import { dataProvider } from "./dataProvider";
import { authProvider } from "./authProvider";
import posts from "./posts";
export const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" {...posts} />
</Admin>
);
// 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 { AccessDenied } from "../AccessDenied";
export const PostCreate = () => (
<IfCanAccess action="create" fallback={<AccessDenied />}>
<Create>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</Create>
</IfCanAccess>
);
You can also choose to redirect users to a custom route:
// In src/App.tsx
import { Admin, CustomRoutes, Resource } from 'react-admin';
import { Route } from 'react-router';
import { dataProvider } from './dataProvider';
import { authProvider } from './authProvider';
import posts from './posts';
import { AccessDenied } from '../AccessDenied';
export const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<CustomRoutes>
<Route path="access-denied" element={<AccessDenied />} />
</CustomRoutes>
<Resource name="posts" {...posts} />
</Admin>
);
// 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 { Navigate } from 'react-router-dom';
export const PostCreate = () => (
<IfCanAccess action="create" fallback={<Navigate to="/access-denied" />}>
<Create>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</Create>
</IfCanAccess>
);
// In src/App.tsx
import { Admin, CustomRoutes, Resource } from "react-admin";
import { Route } from "react-router";
import { dataProvider } from "./dataProvider";
import { authProvider } from "./authProvider";
import posts from "./posts";
import { AccessDenied } from "../AccessDenied";
export const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<CustomRoutes>
<Route path="access-denied" element={<AccessDenied />} />
</CustomRoutes>
<Resource name="posts" {...posts} />
</Admin>
);
// 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 { Navigate } from "react-router-dom";
export const PostCreate = () => (
<IfCanAccess action="create" fallback={<Navigate to="/access-denied" />}>
<Create>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</Create>
</IfCanAccess>
);
Disable Menu Items Instead Of Not Showing Them
The ra-rbac
<Menu>
component does not show menu items the current user has not access to.
As above, it is considered good security practice not to disclose to a potentially malicious user that a page exists if they are not allowed to see it.
However, you might want to disable menu items instead of not showing them.
To achieve this, you can build a custom menu with the core <Menu>
component provided by React-admin and leverage the
usePermissions
hook with the canAccess
function to disable a <Menu.Item>
Let's take a look at the following code:
// In src/App.tsx
import { Admin, Resource } from "react-admin";
import { dataProvider } from "./dataProvider";
import {
Admin,
usePermissions,
ListGuesser,
Menu,
Layout,
Title,
} from 'react-admin';
import { canAccess, Permissions, Resource } from '@react-admin/ra-rbac';
import { Card, CardContent } from '@mui/material';
import InventoryIcon from '@mui/icons-material/Inventory';
import ClassIcon from '@mui/icons-material/Class';
import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout';
const authProvider = () => ({
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () =>
promiseFor([{ action: 'list', resource: 'products' }]),
});
const MyDashboard = () => {
return (
<Card sx={{ marginTop: 5 }}>
<Title title="Welcome to the administration" />
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
</Card>
);
};
const MyMenu = () => {
const { permissions } = usePermissions();
return (
<Menu>
<Menu.DashboardItem />
<Menu.Item
to="/categories"
primaryText="Categories"
leftIcon={<ClassIcon />}
disabled={
!canAccess({
permissions,
resource: 'categories',
})
}
/>
<Menu.Item
to="/products"
primaryText="Products"
leftIcon={<InventoryIcon />}
disabled={
!canAccess({
permissions,
resource: 'products',
})
}
/>
<Menu.Item
to="/orders"
primaryText="Orders"
leftIcon={<ShoppingCartCheckoutIcon />}
disabled={
!canAccess({
permissions,
resource: 'orders',
})
}
/>
</Menu>
);
};
const MyLayout = props => <Layout {...props} menu={MyMenu} />;
export const App = () => (
<Admin
authProvider={authProvider}
dataProvider={dataProvider}
layout={MyLayout}
dashboard={MyDashboard}
>
<Resource name="categories" list={ListGuesser} />
<Resource name="products" list={ListGuesser} />
<Resource name="orders" list={ListGuesser} />
</Admin>
);
// In src/App.tsx
import { Admin, Resource } from "react-admin";
import { dataProvider } from "./dataProvider";
import { usePermissions, ListGuesser, Menu, Layout, Title } from "react-admin";
import { canAccess } from "@react-admin/ra-rbac";
import { Card, CardContent } from "@mui/material";
import InventoryIcon from "@mui/icons-material/Inventory";
import ClassIcon from "@mui/icons-material/Class";
import ShoppingCartCheckoutIcon from "@mui/icons-material/ShoppingCartCheckout";
const authProvider = () => ({
checkAuth: () => Promise.resolve(),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => promiseFor([{ action: "list", resource: "products" }]),
});
const MyDashboard = () => {
return (
<Card sx={{ marginTop: 5 }}>
<Title title="Welcome to the administration" />
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
</Card>
);
};
const MyMenu = () => {
const { permissions } = usePermissions();
return (
<Menu>
<Menu.DashboardItem />
<Menu.Item
to="/categories"
primaryText="Categories"
leftIcon={<ClassIcon />}
disabled={
!canAccess({
permissions,
resource: "categories",
})
}
/>
<Menu.Item
to="/products"
primaryText="Products"
leftIcon={<InventoryIcon />}
disabled={
!canAccess({
permissions,
resource: "products",
})
}
/>
<Menu.Item
to="/orders"
primaryText="Orders"
leftIcon={<ShoppingCartCheckoutIcon />}
disabled={
!canAccess({
permissions,
resource: "orders",
})
}
/>
</Menu>
);
};
const MyLayout = (props) => <Layout {...props} menu={MyMenu} />;
export const App = () => (
<Admin authProvider={authProvider} dataProvider={dataProvider} layout={MyLayout} dashboard={MyDashboard}>
<Resource name="categories" list={ListGuesser} />
<Resource name="products" list={ListGuesser} />
<Resource name="orders" list={ListGuesser} />
</Admin>
);
CHANGELOG
v5.0.2
2024-10-30
- Fix
<Edit redirect>
console warning whenredirect
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
andmatchWildcard
utility functions - Export
<DefaultUnauthorizedView>
- Add support of
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
andmatchWildcard
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>
asRoleDefinitions
(making it easier to usegetPermissionsFromRoles
)
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 anaction
option anymore and support the wildcard (*
) value for it too.IfCanAccess
takes an optionalfallback
prop that accept aReactNode
.
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 theRecordContext
.
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 itsrecordRepresentation
prop
v4.2.0
2022-10-25
- Fix Record-level Permissions for components that should already support them:
SimpleShowLayout
,Tab
,SimpleForm
andTabbedForm
- 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 handlebulkActionButtons={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 aloading
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 thereact-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 passrecord
andresource
to its children.
v1.0.0
2021-07-31
- First release