diff --git a/docs/AccordionForm.md b/docs/AccordionForm.md index 82431935392..902f53ccba7 100644 --- a/docs/AccordionForm.md +++ b/docs/AccordionForm.md @@ -84,10 +84,13 @@ Here are all the props you can set on the `` component: | Prop | Required | Type | Default | Description | | ------------------------- | -------- | ----------------- | ------- | ---------------------------------------------------------------------------- | +| `authorizationError` | Optional | `ReactNode` | `null` | The content to display when authorization checks fail | | `autoClose` | Optional | `boolean` | - | Set to `true` to close the current accordion when opening another one. | | `children` | Required | `ReactNode` | - | A list of `` elements. | | `defaultValues` | Optional | `object|function` | - | The default values of the record. | +| `enableAccessControl` | Optional | `boolean` | `false` | Enable checking authorization rights for each panel and input | | `id` | Optional | `string` | - | The id of the underlying `
` tag. | +| `loading` | Optional | `ReactNode` | | The content to display when checking authorizations | | `noValidate` | Optional | `boolean` | - | Set to `true` to disable the browser's default validation. | | `onSubmit` | Optional | `function` | `save` | A callback to call when the form is submitted. | | `sanitize EmptyValues` | Optional | `boolean` | - | Set to `true` to remove empty values from the form state. | @@ -98,6 +101,48 @@ Here are all the props you can set on the `` component: Additional props are passed to `react-hook-form`'s [`useForm` hook](https://react-hook-form.com/docs/useform). +## `authorizationError` + +Content displayed when `enableAccessControl` is set to `true` and an error occurs while checking for users permissions. Defaults to `null`: + +{% raw %} +```tsx +import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionForm } from '@react-admin/ra-form-layout'; +import { Alert } from '@mui/material'; + +const CustomerEdit = () => ( + + + An error occurred while loading your permissions + + } + > + + + + + + + + + + + + + + + +); +``` +{% endraw %} + ## `autoClose` When setting the `` prop, only one accordion remains open at a time. The first accordion is open by default, and when a user opens another one, the current open accordion closes. @@ -165,6 +210,53 @@ export const PostCreate = () => ( **Tip**: React-admin also allows to define default values at the input level. See the [Setting default Values](./Forms.md#default-values) section. +## `enableAccessControl` + +When set to `true`, React-admin will call the `authProvider.canAccess` method for each panel with the following parameters: +- `action`: `write` +- `resource`: `RESOURCE_NAME.panel.PANEL_ID_OR_LABEL`. For instance: `customers.panel.identity` +- `record`: The current record + +For each panel, react-admin will also call the `authProvider.canAccess` method for each input with the following parameters: +- `action`: `write` +- `resource`: `RESOURCE_NAME.INPUT_SOURCE`. For instance: `customers.first_name` +- `record`: The current record + +**Tip**: `` direct children that don't have a `source` will always be displayed. + +```tsx +import { + ArrayInput, + Edit, + DateInput, + SimpleFormIterator, + TextInput +} from 'react-admin'; +import { AccordionForm } from '@react-admin/ra-form-layout'; + +const CustomerEdit = () => ( + + + + + + + + + + + + + + + + + +); +``` + +**Tip**: If you only want access control for the panels but not for the inputs, set the `enableAccessControl` prop to `false` on the ``. + ## `id` Normally, a submit button only works when placed inside a `` tag. However, you can place a submit button outside the form if the submit button `form` matches the form `id`. @@ -186,6 +278,43 @@ export const PostCreate = () => ( ); ``` +## `loading` + +Content displayed when `enableAccessControl` is set to `true` while checking for users permissions. Defaults to `Loading` from `react-admin`: + +```tsx +import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionForm } from '@react-admin/ra-form-layout'; +import { Typography } from '@mui/material'; + +const CustomerEdit = () => ( + + + Loading your permissions... + + } + > + + + + + + + + + + + + + + + +); +``` + ## `noValidate` The `` attribute prevents the browser from validating the form. This is useful if you don't want to use the browser's default validation, or if you want to customize the error messages. To set this attribute on the underlying `` tag, set the `noValidate` prop to `true`. @@ -377,11 +506,14 @@ Here are all the props you can set on the `` component: | Prop | Required | Type | Default | Description | | ----------------- | -------- | ----------------------- | ------- | ------------------------------------------------------------------------------------------------ | +| `authorizationError` | Optional | `ReactNode` | `null` | The content to display when authorization checks fail | | `children` | Required | `ReactNode` | - | A list of `` elements | | `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default (except if autoClose = true on the parent) | | `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. | +| `enableAccessControl` | Optional | `boolean` | `false` | Enable checking authorization rights for this panel's inputs | | `id` | Optional | `string` | - | An id for this Accordion to be used in the [`useFormGroup`](./Upgrade.md#useformgroup-hook-returned-state-has-changed) hook and for CSS classes. | | `label` | Required | `string` or `ReactNode` | - | The main label used as the accordion summary. Appears in red when the accordion has errors | +| `loading` | Optional | `ReactNode` | | The content to display when checking authorizations | | `secondary` | Optional | `string` or `ReactNode` | - | The secondary label used as the accordion summary | | `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. | | `sx` | Optional | `Object` | - | An object containing the MUI style overrides to apply to the root component. | @@ -433,6 +565,8 @@ Here are all the props you can set on the `` component: | Prop | Required | Type | Default | Description | | ------------------ | -------- | ----------------------- | ------- | ------------------------------------------------------------- | +| `accessDenied` | Optional | `Component` | - | The component to use when users don't have the permissions required to access this section. | +| `authorizationError` | Optional | `Component` | - | The component to use when an error occurs while checking permissions. | | `Accordion` | Optional | `Component` | - | The component to use as the accordion. | | `AccordionDetails` | Optional | `Component` | - | The component to use as the accordion details. | | `AccordionSummary` | Optional | `Component` | - | The component to use as the accordion summary. | @@ -440,9 +574,11 @@ Here are all the props you can set on the `` component: | `className` | Optional | `string` | - | A class name to style the underlying `` | | `defaultExpanded` | Optional | `boolean` | `false` | Set to true to have the accordion expanded by default | | `disabled` | Optional | `boolean` | `false` | If true, the accordion will be displayed in a disabled state. | +| `enableAccessControl` | Optional | `boolean` | - | Enable access control to the section and its inputs | | `fullWidth` | Optional | `boolean` | `false` | If true, the Accordion takes the entire form width. | | `id` | Optional | `string` | - | An id for this Accordion to be used for CSS classes. | | `label` | Required | `string` or `ReactNode` | - | The main label used as the accordion summary. | +| `loading` | Optional | `Component` | - | The component to use while checking permissions. | | `secondary` | Optional | `string` or `ReactNode` | - | The secondary label used as the accordion summary | | `square` | Optional | `boolean` | `false` | If true, rounded corners are disabled. | @@ -491,6 +627,173 @@ const CustomerEdit = () => ( ); ``` +### `accessDenied` + +Content displayed when `enableAccessControl` is set to `true` and users don't have access to the section. Defaults to `null`: + +{% raw %} +```tsx +import { ReactNode } from 'react'; +import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionSection } from '@react-admin/ra-form-layout'; +import { Alert } from '@mui/material'; + +const AccessDenied = ({ children }: { children: ReactNode }) => ( + + Upgrade to Premium + + } + > + {children} + +) + +const CustomerEdit = () => ( + + + + + You don't have access to the preferences section} + > + + + + + + +); +``` +{% endraw %} + + +### `authorizationError` + +Content displayed when `enableAccessControl` is set to `true` and an error occurs while checking for users permissions. Defaults to `null`: + +{% raw %} +```tsx +import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionSection } from '@react-admin/ra-form-layout'; +import { Alert } from '@mui/material'; + +const AuthorizationError = () => ( + + An error occurred while loading your permissions + +); + +const CustomerEdit = () => ( + + + + + }> + + + + + + +); +``` +{% endraw %} + +### `enableAccessControl` + +When set to `true`, React-admin will call the `authProvider.canAccess` method the following parameters: +- `action`: `write` +- `resource`: `RESOURCE_NAME.section.PANEL_ID_OR_LABEL`. For instance: `customers.section.identity` +- `record`: The current record + +React-admin will also call the `authProvider.canAccess` method for each input with the following parameters: +- `action`: `write` +- `resource`: `RESOURCE_NAME.INPUT_SOURCE`. For instance: `customers.first_name` +- `record`: The current record + +```tsx +import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionSection } from '@react-admin/ra-form-layout'; + +const CustomerEdit = () => ( + + + + + + + + + + + + + + +); +``` + +**Tip**: `` direct children that don't have a `source` will always be displayed. + +### `loading` + +Content displayed when `enableAccessControl` is set to `true` while checking for users permissions. Defaults to `Loading` from `react-admin`: + +```tsx +import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin'; +import { AccordionSection } from '@react-admin/ra-form-layout'; +import { Typography } from '@mui/material'; + +const AuthorizationLoading = () => ( + + Loading your permissions... + +); + +const CustomerEdit = () => ( + + + + + + + + }> + + + + + + +); +``` + + ## AutoSave In forms where users may spend a lot of time, it's a good idea to save the form automatically after a few seconds of inactivity. You turn on this feature by using [the `` component](./AutoSave.md). @@ -529,59 +832,43 @@ const PersonEdit = () => ( Note that you **must** set the `` prop to `{ keepDirtyValues: true }`. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost. -If you're using it in an `` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](https://marmelab.com/react-admin/Edit.html#mutationmode) - `` doesn't work with the default `mutationMode="undoable"`. +If you're using it in an `` page, you must also use a `pessimistic` or `optimistic` [`mutationMode`](./Edit.md#mutationmode) - `` doesn't work with the default `mutationMode="undoable"`. Check [the `` component](./AutoSave.md) documentation for more details. -## Role-Based Access Control (RBAC) +## Access Control -Fine-grained permissions control can be added by using the [``](./AuthRBAC.md#accordionform), [``](./AuthRBAC.md#accordionform) and [``](./AuthRBAC.md#accordionsection) components provided by the `@react-admin/ra-enterprise` package. +`` can use [Access Control](./AccessControl.md) to check permissions for each section and input. To enable this feature, set the `enableAccessControl` prop to `true`. + +Check the [`enableAccessControl` prop](#enableaccesscontrol) section for more details. -{% raw %} ```tsx -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 - ]), -}; +import { + ArrayInput, + Edit, + DateInput, + SimpleFormIterator, + TextInput +} from 'react-admin'; +import { AccordionForm } from '@react-admin/ra-form-layout'; -const ProductEdit = () => ( +const CustomerEdit = () => ( - - - - - - - - - - + + + + - - + + + + + + + + - { /* delete button not displayed */ } ); ``` -{% endraw %} - -Check [the RBAC `` component](./AuthRBAC.md#accordionform) documentation for more details. diff --git a/docs/AuthRBAC.md b/docs/AuthRBAC.md index 5915288fb8b..a5fe36b877f 100644 --- a/docs/AuthRBAC.md +++ b/docs/AuthRBAC.md @@ -5,15 +5,19 @@ title: "RBAC" # Role-Based Access Control (RBAC) -React-admin Enterprise Edition contains [the ra-rbac module](https://react-admin-ee.marmelab.com/documentation/ra-rbac), which adds fine-grained permissions to your admin. This module extends the `authProvider` and adds replacement for many react-admin components that use these permissions. +Building up on react-admin's [Access Control features](./Permissions.md#access-control), react-admin RBAC provides an implementation for `authProvider.canAccess()` to manage roles and fine-grained permissions, and exports alternative react-admin [components](#components) that use these permissions. -Test it live in the [Enterprise Edition Storybook](https://storybook.ra-enterprise.marmelab.com/?path=/story/ra-rbac-full-app--full-app). +The RBAC features are part of [ra-rbac](https://react-admin-ee.marmelab.com/documentation/ra-rbac), an [Enterprise Edition](https://react-admin-ee.marmelab.com) package. Test them live in the [Enterprise Edition Storybook](https://react-admin.github.io/ra-enterprise/?path=/story/ra-rbac-full-app--full-app). -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). +## At a Glance + +RBAC relies on an array of roles and permissions to determine what a user can do in a React-admin application. You can define permissions for pages, fields, buttons, etc. These permissions use a serialization format that is easy to understand and to maintain. You can store them in a database, in a JSON file, or in your code. + +Roles and permissions are used by `authProvider.canAccess()` to provide fine-grained access control to the entire app. The above demo uses the following set of permissions: @@ -62,17 +66,19 @@ const roles = { ## Installation +First, install the `@react-admin/ra-rbac` package: + ``` npm install --save @react-admin/ra-rbac # or yarn add @react-admin/ra-rbac ``` -Make sure you [enable auth features](https://marmelab.com/react-admin/Authentication.html#enabling-auth-features) by setting an ``, and [disable anonymous access](https://marmelab.com/react-admin/Authentication.html#disabling-anonymous-access) by adding the `` prop. This will ensure that react-admin waits for the `authProvider` response before rendering anything. - **Tip**: ra-rbac is part of the [React-Admin Enterprise Edition](https://react-admin-ee.marmelab.com/), and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package. -## Vocabulary +Make sure you [enable auth features](https://marmelab.com/react-admin/Authentication.html#enabling-auth-features) by setting an ``, and [disable anonymous access](https://marmelab.com/react-admin/Authentication.html#disabling-anonymous-access) by adding the `` prop. This will ensure that react-admin waits for the `authProvider` response before rendering anything. + +## Concepts ### Permission @@ -88,6 +94,28 @@ Here are a few examples of permissions: **Tip**: When the `record` field is omitted, the permission is valid for all records. +### Action + +An _action_ is a string, usually a verb, that represents an operation. Examples of actions include "read", "create", "edit", "delete", or "export". + +React-admin already does page-level access control with actions like "list", "show", "edit", "create", and "delete". RBAC checks additional actions in its components: + +| Action | Description | Used In | +| -------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `list` | Allow to access the List page | [``](./List.md#access-control), [``](./Buttons.md#listbutton), [``](./Menu.md#access-control) | +| `show` | Allow to access the Show page | [``](./Show.md), [``](./Buttons.md#showbutton), [``](./Datagrid.md#access-control), [``](./Edit.md) | +| `create` | Allow to access the Create page | [``](./Create.md), [``](./Buttons.md#createbutton), [``](./List.md#access-control) | +| `edit` | Allow to access the Edit page | [``](./Edit.md), [``](./Buttons.md#editbutton), [``](./Datagrid.md#access-control), [``](./Show.md) | +| `delete` | Allow to delete data | [``](./Buttons.md#deletebutton), [``](./Buttons.md#bulkdeletebutton), [``](./Datagrid.md#access-control), [``](./SimpleForm.md#access-control), [``](#tabform) | +| `export` | Allow to export data | [``](./Buttons.md#exportbutton), [``](./List.md#access-control) | +| `clone` | Allow to clone a record | [``](./Buttons.md#clonebutton), [``](./Edit.md) | +| `read` | Allow to view a field (or a tab) | [``](./Datagrid.md#access-control), [``](./SimpleShowLayout.md#access-control), [``](./TabbedShowLayout.md#access-control) | +| `write` | Allow to edit a field (or a tab) | [``](./SimpleForm.md#access-control), [``](./TabbedForm.md#access-control), [``](./WizardForm.md#enableaccesscontrol), [``](./LongForm.md#enableaccesscontrol), [``](./AccordionForm.md#enableaccesscontrol) | + +**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. + +You can also add your own actions, and use them in your own components using [`useCanAccess`](./useCanAccess.md) or [``](./CanAccess.md). + ### 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. @@ -134,37 +162,9 @@ const corrector123Role = [ **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](#explicit-deny). -The RBAC system relies on *permissions* only. It's the `authProvider`'s responsibility to map roles to permissions. See the [`authProvider` Methods](#authprovider-methods) section for details. - -### 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`](https://react-admin-ee.marmelab.com/documentation/ra-rbac#usecanaccess), [`canAccess`](https://react-admin-ee.marmelab.com/documentation/ra-rbac#canaccess) or [``](https://react-admin-ee.marmelab.com/documentation/ra-rbac#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), [``](#tabform) | -| `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. +### Pessimistic Strategy -## Concepts +RBAC components treat permissions in a pessimistic way: while permissions are loading, react-admin doesn't render the components that require permissions, assuming that these components are restricted by default. It's only when the `authProvider.canAccess()` has resolved that RBAC components render. ### Principle Of Least Privilege @@ -181,14 +181,22 @@ 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`](https://lodash.com/docs/4.17.15#isMatch)). ```jsx -// can read all users, without record restriction -const perm1 = { action: "read", resource: "users" }; -// can write only user of id 123 -const perm2 = { action: "write", resource: "users", record: { "id": "123" } }; -// can access only comments by user of id 123 -const perm3 = { action: "*", resource: "comments", record: { "user_id": "123" } }; +// can view all users, without record restriction +const perm1 = { action: ['list', 'show'], resource: 'users' }; +const perm2 = { action: 'read', resource: 'users.*' }; +// can only edit field 'username' for user of id 123 +const perm4 = { action: 'write', resource: 'users.username', record: { id: '123' } }; ``` +Only record-level components can perform record-level permissions checks. Below is the list of components that support them: + +- [``](./SimpleShowLayout.md#access-control) +- [``](./TabbedShowLayout.md#access-control) +- [``](./SimpleForm.md#access-control) +- [``](./TabbedForm.md#access-control) + +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. @@ -213,276 +221,206 @@ const allProductsButStock = [ **Tip**: Deny permissions are evaluated first, no matter in which order the permissions are defined. -## `authProvider` Methods +## Setup -Ra-rbac builds up on react-admin's `authProvider` API. It precises the return format of the `getPermissions()` method which must return a promise for an array of permissions objects. +Define role definitions in your application code, or fetch them from an API. ```jsx -const authProvider = { - // ... - getPermissions: () => Promise.resolve([ - { action: ["read", "write"], resource: "users", record: { "id": "123" } }, - ]) +export const roleDefinitions = { + admin: [ + { action: '*', resource: '*' } + ], + reader: [ + { action: ['list', 'show', 'export'], resource: '*' } + { action: 'read', resource: 'posts.*' } + { action: 'read', resource: 'comments.*' } + ], + accounting: [ + { action: '*', resource: 'sales' }, + ], }; ``` -For every restricted resource, ra-rbac calls `authProvider.getPermissions()` to get the permissions. - -In practice, most auth providers get the permissions as a response from the login query, and store these permissions in memory or localStorage. When a component calls `authProvider.getPermissions()`, the auth provider only needs to read from that local copy of the permissions. - -`authProvider.getPermissions()` doesn't return roles - only permissions. Usually, the role definitions are committed with the application code, as a constant. The roles of the current user are fetched at login, and the permissions are computed from the roles and the role definitions. - -You can use the `getPermissionsFromRoles` helper in the `authProvider` to compute the permissions that the user has based on their permissions. This function takes an object as argument with the following fields: - -- `roleDefinitions`: a static object containing the role definitions for each role -- `userRoles` _(optional)_: an array of roles (admin, reader...) for the current user -- `userPermissions` _(optional)_: an array of permissions for the current user, to be added to the permissions computed from the roles - -Here is an example `authProvider` implementation following this pattern: +The user roles and permissions should be returned upon login. The `authProvider` should store the permissions in memory, or in localStorage. This allows `authProvider.canAccess()` to read the permissions from localStorage. ```tsx import { getPermissionsFromRoles } from '@react-admin/ra-rbac'; +import { roleDefinitions } from './roleDefinitions'; -const roleDefinitions = { - admin: [{ action: '*', resource: '*' }], - reader: [{ action: 'read', resource: '*' }], -}; const authProvider = { - login: ({ username, password }) => { + login: async ({ username, password }) => { const request = new Request('https://mydomain.com/authenticate', { method: 'POST', body: JSON.stringify({ username, password }), headers: new Headers({ 'Content-Type': 'application/json' }), }); - return fetch(request) - .then(response => { - if (response.status < 200 || response.status >= 300) { - throw new Error(response.statusText); - } - return response.json(); - }) - .then(data => { - // data is like - // { - // "id": 123, - // "fullName": "John Doe", - // "permissions": [ - // { action: ["read", "write"], resource: "users", record: { id: "123" } }, - // ] - // "roles": ["admin", "reader"], - // } - const permissions = getPermissionsFromRoles({ - roleDefinitions, - userPermissions: data.permissions, - userRoles: data.roles, - }); - localStorage.setItem( - 'permissions', - JSON.stringify(permissions) - ); - }); + const response = await fetch(request); + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + const { user: { roles, permissions }} = await response.json(); + // merge the permissions from the roles with the extra permissions + const permissions = getPermissionsFromRoles({ + roleDefinitions, + userPermissions, + userRoles + }); + localStorage.setItem('permissions', JSON.stringify(permissions)); }, // ... - 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](#performance) below. - -## Hooks - -Ra-rbac provides hooks to enable or disable features based on roles and permissions. - -- [`usePermissions()`](./usePermissions.md) returns the current permissions. -- [`useCanAccess()`](https://react-admin-ee.marmelab.com/documentation/ra-rbac#usecanaccess) returns a boolean indicating whether the user has access to the given resource. - -## Components +Then, use these permissions in `authProvider.canAccess()`: -Ra-rbac provides alternative components to react-admin base components. These alternative components include role-based access control and are as follows: - -- Main - - [``](#resource) - - [``](#menu) -- List - - [``](#list) - - [``](#listactions) - - [``](#datagrid) -- Detail - - [``](#edit) - - [``](#show) - - [``](#simpleshowlayout) - - [``](#tab) -- Form - - [``](#simpleform) - - [``](#tabbedform) - - [``](#formtab) - - [``](#accordionform) - - [``](#accordionsection) - - [``](#longform) - - [``](#wizardform) +```tsx +import { canAccessWithPermissions } from '@react-admin/ra-rbac'; -## `` +const authProvider = { + // ... + canAccess: async ({ resource, action, record }) => { + const permissions = JSON.parse(localStorage.getItem('permissions')); + // check if the user can access the resource and action + return canAccessWithPermissions({ permissions, resource, action, record }); + }, +}; +``` -Alternative to react-admin's [``](https://marmelab.com/react-admin/AccordionForm.html) that adds RBAC control to the accordions, the inputs, and the delete button. +**Tip**: If `canAccess` needs to call the server every time, check out [the Performance section](#performance) below. -This component is provided by the `@react-admin/ra-enterprise` package. +## `getPermissionsFromRoles` -{% raw %} -```tsx -import { Edit, TextInput } from 'react-admin'; -import { AccordionForm } from '@react-admin/ra-enterprise'; +This function returns an array of user permissions based on a role definition, a list of roles, and a list of user permissions. It merges the permissions defined in `roleDefinitions` for the current user's roles (`userRoles`) with the extra `userPermissions`. -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 - // note that the panel with the name 'description' will be displayed - { action: 'write', resource: 'products.panel.description' }, - // note that the panel with the name 'images' will be displayed - { action: 'write', resource: 'products.panel.images' }, - // 'products.panel.stock' is missing - ]), +```jsx +// static role definitions (usually in the app code) +const roleDefinitions = { + admin: [ + { action: '*', resource: '*' } + ], + reader: [ + { action: ['list', 'show', 'export'], resource: '*' } + { action: 'read', resource: 'posts.*' } + { action: 'read', resource: 'comments.*' } + ], + accounting: [ + { action: '*', resource: 'sales' }, + ], }; -const ProductEdit = () => ( - - - - - - - {/* not displayed */} - - - - {/* not displayed */} - - - - {/* not displayed */} - - - - {/* delete button not displayed */} - - -); +const permissions = getPermissionsFromRoles({ + roleDefinitions, + // roles of the current user (usually returned by the server upon login) + userRoles: ['reader'], + // extra permissions for the current user (usually returned by the server upon login) + userPermissions: [ + { action: 'list', resource: 'sales'}, + ], +}); +// permissions = [ +// { action: ['list', 'show', 'export'], resource: '*' }, +// { action: 'read', resource: 'posts.*' }, +// { action: 'read', resource: 'comments.*' }, +// { action: 'list', resource: 'sales' }, +// ]; ``` -{% endraw %} -**Tip**: You must add a `name` prop to the `` so you can reference it in the permissions. -Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.panel.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. +This function takes an object as argument with the following fields: -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'write', resource: 'products.panel.description' }`. - -`` also only renders the child inputs for which the user has the 'write' permissions. +- `roleDefinitions`: a dictionary containing the role definition for each role +- `userRoles` _(optional)_: an array of roles (admin, reader...) for the current user +- `userPermissions` _(optional)_: an array of permissions for the current user -## `` +## `canAccessWithPermissions` -Replacement for the default `` that only renders a section if the user has the right permissions. `` 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. +`canAccessWithPermissions` is a helper that facilitates the `authProvider.canAccess()` method implementation: -{% raw %} ```tsx -import { Edit, SimpleForm, TextInput } from 'react-admin'; -import { AccordionSection } from '@react-admin/ra-enterprise'; +import { canAccessWithPermissions } 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 - // note that the section with the name 'description' will be displayed - { action: 'write', resource: 'products.section.description' }, - // note that the section with the name 'images' will be displayed - { action: 'write', resource: 'products.section.images' }, - // 'products.section.stock' is missing - ]), + canAccess: async ({ action, resource, record }) => { + const permissions = JSON.parse(localStorage.getItem('permissions')); + return canAccessWithPermissions({ + permissions, + action, + resource, + record, + }); + } }; - -const ProductEdit = () => ( - - - - - - - // not displayed - - - - // not displayed - - - - // not displayed - - - - - -); ``` -{% endraw %} +`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](#getpermissionsfromroles) function. -Add a `name` prop to the `` so you can reference it in the permissions. -Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.section.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. +## Components -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'write', resource: 'products.section.description' }`. +Ra-rbac provides alternative components to react-admin base components with RBAC support: -## `` +- Main + - [``](./Menu.md#access-control) +- List + - [``](./List.md#access-control) + - [``](./Datagrid.md#access-control) + - [``](./Buttons.md#exportbutton) +- Detail + - [``](./SimpleShowLayout.md#access-control) + - [``](./TabbedShowLayout.md#access-control) + - [``](./Buttons.md#clonebutton) +- Form + - [``](./SimpleForm.md#access-control) + - [``](./TabbedForm.md#access-control) -Alternative to react-admin's `` that adds RBAC control to columns +In addition, the following components from te Enterprise edition have built-in RBAC support: -To see a column, the user must have the permission to read the resource column: +- [``](./AccordionForm.md#access-control) +- [``](./LongForm.md#access-control) +- [``](./WizardForm.md#access-control) -```jsx -{ action: "read", resource: `${resource}.${source}` } -``` +Here is an example of `` with RBAC: -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 - -```jsx -import { ImageField, TextField, ReferenceField, NumberField } from 'react-admin'; -import { List, Datagrid } from '@react-admin/ra-rbac'; +```tsx +import { + canAccessWithPermissions, + List, + Datagrid +} from '@react-admin/ra-rbac'; +import { + ImageField, + TextField, + ReferenceField, + NumberField, +} from 'react-admin'; -const authProvider= { +const authProvider = { // ... - getPermissions: () => Promise.resolve({ - permissions: [ - { action: "list", resource: "products" }, - { action: "read", resource: "products.thumbnail" }, - { action: "read", resource: "products.reference" }, - { action: "read", resource: "products.category_id" }, - { action: "read", resource: "products.width" }, - { action: "read", resource: "products.height" }, - { action: "read", resource: "products.price" }, - { action: "read", resource: "products.description" }, - ] - }), + canAccess: async ({ action, record, resource }) => + canAccessWithPermissions({ + permissions: [ + { action: 'list', resource: 'products' }, + { action: 'read', resource: 'products.thumbnail' }, + { action: 'read', resource: 'products.reference' }, + { action: 'read', resource: 'products.category_id' }, + { action: 'read', resource: 'products.width' }, + { action: 'read', resource: 'products.height' }, + { action: 'read', resource: 'products.price' }, + { action: 'read', resource: 'products.description' }, + // { action: 'read', resource: 'products.stock' }, + // { action: 'read', resource: 'products.sales' }, + // { action: 'delete', resource: 'products' }, + { action: 'show', resource: 'products' }, + ], + action, + record, + resource + }), }; const ProductList = () => ( - {/* ra-rbac Datagrid */} + {/* The datagrid has no bulk actions as the user doesn't have the 'delete' permission */} + @@ -492,7 +430,7 @@ const ProductList = () => ( - {/* these two columns are not visible to the user */} + {/** These two columns are not visible to the user **/} @@ -500,584 +438,51 @@ const ProductList = () => ( ); ``` -## `` - -Replacement for react-admin's `` that adds RBAC control to actions. - -- Users must have the 'show' permission on the resource and record to see the ``. -- Users must have the 'clone' permission on the resource and record to see the ``. - -```jsx -import { Edit } from '@react-admin/ra-rbac'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve({ - permissions: [ - { action: ['list', 'edit', 'clone'], resource: 'products' }, - ], - }), -}; - -export const PostEdit = () => ( - - ... - -); -// user will see the clone button but not the show button -``` - -## `` - -Replacement for react-admin's `` that adds RBAC control to actions and bulk actions. - -- Users must have the 'create' permission on the resource to see the ``. -- Users must have the 'export' permission on the resource to see the `` and the ``. -- Users must have the 'delete' permission on the resource to see the ``. - -```jsx -import { List } from '@react-admin/ra-rbac'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve({ - permissions: [ - { action: 'list', resource: 'products' }, - { action: 'create', resource: 'products' }, - { action: 'delete', resource: 'products' }, - // action 'export' is missing - ], - }), -}; - -export const PostList = () => ( - - ... - -); -// user will see the following actions on top of the list: -// - create -// user will see the following bulk actions upon selection: -// - delete -``` - -**Tip**: This `` component relies on [the `` component](#listactions) below. - -## `` - -Replacement for react-admin's `` that adds RBAC control to actions. - -- Users must have the 'create' permission on the resource to see the ``. -- Users must have the 'export' permission on the resource to see the ``. - -```jsx -import { List } from 'react-admin'; -import { ListActions } from '@react-admin/ra-rbac'; - -export const PostList = () => ( - }> - ... - -); -``` - -## `` - -Alternative to react-admin's [``](https://marmelab.com/react-admin/LongForm.html) that adds RBAC control to the delete button, hides sections users don't have access to, and renders inputs based on permissions. Part of the `@react-admin/ra-enterprise` package. - -{% raw %} -```tsx -import { LongForm } from '@react-admin/ra-enterprise'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve([ - { action: ['list', 'edit'], resource: 'products' }, - /* sections */ - { action: 'write', resource: 'products.section.description' }, - { action: 'write', resource: 'products.section.images' }, - // 'products.section.stock' is missing - - /* inputs */ - { action: 'write', resource: 'products.reference' }, - { action: 'write', resource: 'products.width' }, - { action: 'write', resource: 'products.height' }, - // 'products.description' is missing - // 'products.image' is missing - { action: 'write', resource: 'products.thumbnail' }, - ]), -}; - -const ProductEdit = () => ( - - - - - - - {/* not displayed */} - - - - {/* not displayed */} - - - - {/* not displayed */} - - - - {/* delete button not displayed */} - - -); -``` - -{% endraw %} - -**Tip**: You must add a `name` prop to the `` so you can reference it in the permissions. -Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.section.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. - -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'write', resource: 'products.section.description' }`. - -`` also only renders the child inputs for which the user has the 'write' permissions. - -## `` - -A replacement for react-admin's `` component, which only displays the menu items that the current user has access to (using the `list` action). - -Pass this menu to a ``, and pass that layout to the `` component to use it. - -```jsx -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({ - permissions: [ - { action: "*", resource: "posts" }, - { action: "*", resource: "comments" }, - ] - }), -}; - -const CustomLayout = ({ children }) => ( - - {children} - -); - -const App = () => ( - - - - {/* the user won't see the Users menu */} - - -); -``` - -## `` - -To restrict access to Create, Edit, List and Show views for your resources, use the `` component from ra-rbac rather than the one from react-admin: - -```jsx -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 = () => ( - - - - -); -``` - -Ra-rbac's `` relies on the following actions: - -- `list` to enable the list view -- `show` to enable the show view -- `create` to enable the create view -- `edit` to enable the edit view - -**Tip**: When using ra-rbac's ``, the `permissions` injected to ``, `` and `` component are the merged permissions of the user and the user's roles (as returned by `usePermission`). This makes the use of `canAccess` more straightforward. Here is the Datagrid example from the `canAccess` section above, revisited for an application using ra-rbac's ``: - -```jsx -import { List, Datagrid, TextField } from 'react-admin'; -import { usePermissions, canAccess } from '@react-admin/ra-rbac'; - -const authProvider = { - checkAuth: () => Promise.resolve(), - login: () => Promise.resolve(), - logout: () => Promise.resolve(), - checkError: () => Promise.resolve(), - getPermissions: () => Promise.resolve({ - permissions: [ - { action: ['list', 'read_price'], resource: 'products' }, - ], - }), -}; - -const ProductList = () => { - const { permissions } = usePermissions(); - return ( - - - - - - - {canAccess({ - permissions, - action: 'read_price', - resource: 'products', - }) && } - {/* this column will not render */} - {canAccess({ - permissions, - action: 'read_stock', - resource: 'products', - }) && } - - - ); -} -``` - -## `` - -Replacement for react-admin's `` that adds RBAC control to actions. - -Users must have the 'edit' permission on the resource and record to see the ``. - -```jsx -import { ShowProps } from 'react-admin'; -import { Show } from '@react-admin/ra-rbac'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve({ - permissions: [ - { action: ['list', 'show', 'edit'], resource: 'products' }, - ], - }), -}; - -export const PostShow = () => ( - - ... - -); -// user will see the edit action on top of the Show view -``` - -To control the appearance of individual fields, use [the `` component](#simpleshowlayout) from ra-enterprise. - -## `` - -Alternative to react-admin's `` that adds RBAC control to inputs - -To see an input, the user must have the permission to write the resource field: - -```js -{ action: "write", resource: `${resource}.${source}` } -``` - -`` also renders the delete button only if the user has the 'delete' permission. - -```jsx -import { Edit, TextInput } from 'react-admin'; -import { SimpleForm } from '@react-admin/ra-rbac'; - -const authProvider= { - // ... - getPermissions: () => Promise.resolve({ - permissions: [ - // 'delete' is missing - { action: ['list', 'edit'], resource: 'products' }, - { action: 'write', resource: 'products.reference' }, - { action: 'write', resource: 'products.width' }, - { action: 'write', resource: 'products.height' }, - // 'products.description' is missing - { action: 'write', resource: 'products.thumbnail' }, - // 'products.image' is missing - ] - }), -}; - -const ProductEdit = () => ( - - - - - - {/* not displayed */} - - {/* not displayed */} - - - {/* no delete button */} - - -); -``` - -## `` - -Alternative to react-admin's `` that adds RBAC control to fields - -To see a column, the user must have the permission to read the resource column: - -```js -{ action: "read", resource: `${resource}.${source}` } -``` - -```jsx -import { ShowProps } from 'react-admin'; -import { SimpleShowLayout } from '@react-admin/ra-rbac'; - -const authProvider= { - // ... - getPermissions: () => Promise.resolve({ - permissions: [ - { action: ['list', 'show'], resource: 'products' }, - { action: 'read', resource: 'products.reference' }, - { action: 'read', resource: 'products.width' }, - { action: 'read', resource: 'products.height' }, - // 'products.description' is missing - // 'products.image' is missing - { action: 'read', resource: 'products.thumbnail' }, - // 'products.stock' is missing - ] - }), -}; - -const ProductShow = () => ( - - {/* <-- RBAC SimpleShowLayout */} - - - - {/* not displayed */} - - {/* not displayed */} - - - {/* not displayed */} - - - -); -``` - -## `` - -`` shows only the tabs for which users have read permissions, using the `[resource].tab.[tabName]` string as resource identifier. `` shows only the child fields for which users have the read permissions, using the `[resource].[source]` string as resource identifier. - -```jsx -import { Show, TextField } from 'react-admin'; -import { TabbedShowLayout } from '@react-admin/ra-rbac'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve([ - // crud - { action: ['list', 'show'], resource: 'products' }, - // tabs ('products.tab.stock' is missing) - { action: 'read', resource: 'products.tab.description' }, - { action: 'read', resource: 'products.tab.images' }, - // fields ('products.description' and 'products.image' are missing) - { action: 'read', resource: 'products.reference' }, - { action: 'read', resource: 'products.width' }, - { action: 'read', resource: 'products.height' }, - { action: 'read', resource: 'products.thumbnail' }, - ]), -}; - -const ProductShow = () => ( - - - - - - - {/* the description field is not displayed */} - - - {/* the stock tab is not displayed */} - - - - - {/* the images field is not displayed */} - - - - - -); -``` - -You must add a `name` prop to the `` so you can reference it in the permissions. -Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'read', resource: '{RESOURCE}.tab.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. - -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'read', resource: 'products.tab.description' }`. - -## `` - -`` shows only the tabs for which users have write permissions, using the `[resource].tab.[tabName]` string as resource identifier. It also renders the delete button only if the user has a permission for the `delete` action in the current resource. `` shows only the child inputs for which users have the write permissions, using the `[resource].[source]` string as resource identifier. - - -```tsx -import { Edit, TextInput } from 'react-admin'; -import { TabbedForm } from '@react-admin/ra-rbac'; - -const authProvider = { - // ... - getPermissions: () => Promise.resolve([ - // crud (the delete action is missing) - { action: ['list', 'edit'], resource: 'products' }, - // tabs ('products.tab.stock' is missing) - { action: 'write', resource: 'products.tab.description' }, - { action: 'write', resource: 'products.tab.images' }, - // fields ('products.description' and 'products.image' are missing) - { action: 'write', resource: 'products.reference' }, - { action: 'write', resource: 'products.width' }, - { action: 'write', resource: 'products.height' }, - { action: 'write', resource: 'products.thumbnail' }, - ]), -}; - -const ProductEdit = () => ( - - - - - - - {/* the description input is not displayed */} - - - {/* the stock tab is not displayed */} - - - - - {/* the images input is not displayed */} - - - - {/* the delete button is not displayed */} - - -); -``` - -You must add a `name` prop to the `` so you can reference it in the permissions. Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.tab.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. - -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'write', resource: 'products.tab.description' }`. - -## `` - -Alternative to react-admin's `` that adds RBAC control to hide steps users don't have access to. `` 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. - -{% raw %} -```tsx -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 - // note that the step with the name 'description' will be displayed - { action: 'write', resource: 'products.step.description' }, - // note that the step with the name 'images' will be displayed - { action: 'write', resource: 'products.step.images' }, - // 'products.step.stock' is missing - ]), -}; - -const ProductCreate = () => ( - - - - - - - {/* Won't be displayed */} - - - - {/* Won't be displayed */} - - - - {/* Won't be displayed */} - - - - {/* Delete button won't be displayed */} - - -); -``` -{% endraw %} - -**Tip**: You must add a `name` prop to the `` so you can reference it in the permissions. -Then, to allow users to access a particular ``, update the permissions definition as follows: `{ action: 'write', resource: '{RESOURCE}.step.{NAME}' }`, where `RESOURCE` is the resource name, and `NAME` the name you provided to the ``. - -For instance, to allow users access to the following tab `` in `products` resource, add this line in permissions: `{ action: 'write', resource: 'products.step.description' }`. - ## 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. - -To compensate for that, `usePermissions` uses a stale-while-revalidate approach, and after the initial call to `authProvider.getPermissions()`, it will return the permissions from the cache, and refresh them in the background. +`authProvider.canAccess()` can return a promise, which in theory allows to rely on the authentication server for permissions. The downside is that this slows down the app a great deal, as each page may contain dozens of calls to these methods. In practice, your `authProvider` should use short-lived sessions, and refresh the permissions only when the session ends. JSON Web tokens (JWT) work that way. Here is an example of an `authProvider` that stores the permissions in memory, and refreshes them only every 5 minutes: -```jsx +```tsx +import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-rbac'; + let permissions; // memory cache let permissionsExpiresAt = 0; const getPermissions = () => { const request = new Request('https://mydomain.com/permissions', { - headers: new Headers({ 'Authorization': `Bearer ${localStorage.getItem('token')}` }), + 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 }); - 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 }) => { + login: ({ username, password }) => { const request = new Request('https://mydomain.com/authenticate', { method: 'POST', body: JSON.stringify({ username, password }), @@ -1092,11 +497,27 @@ const authProvider = { }) .then(data => { localStorage.setItem('token', JSON.stringify(data.token)); + localStorage.setItem('userRoles', JSON.stringify(data.roles)); }); }, // ... - getPermissions: () => { - return Date.now() > permissionsExpiresAt ? getPermissions() : permissions; + canAccess: async ({ action, record, resource }) => { + if (Date.now() > rolesExpiresAt) { + await getRoles(); + } + if (Date.now() > permissionsExpiresAt) { + await getPermissions(); + } + return canAccessWithPermissions({ + permissions: getPermissionsFromRoles({ + roleDefinitions, + userPermissions: permissions, + userRoles: localStorage.getItem('userRoles'), + }, + action, + record, + resource, + }); }, }; ``` diff --git a/docs/Buttons.md b/docs/Buttons.md index d8777e7ce33..d0e83496ee6 100644 --- a/docs/Buttons.md +++ b/docs/Buttons.md @@ -568,6 +568,80 @@ To override the style of all instances of `