Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add access control #10222

Merged
merged 182 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
182 commits
Select commit Hold shift + click to select a range
c929cec
Add useCanAccess hook
djhi Sep 18, 2024
0c3e8f5
Add access control to resource views
djhi Sep 18, 2024
af8dab3
Cleanup tests output
djhi Sep 18, 2024
3d1f48e
Add access control to ResourceMenuItem
djhi Sep 18, 2024
85dc156
Fix first resource detection
djhi Sep 18, 2024
80cf005
Reorganize stories
djhi Sep 19, 2024
2931d3c
Add tests for first resource detection
djhi Sep 19, 2024
2685254
Fix Admin documentation
djhi Sep 19, 2024
7dbc256
Add <CanAccess>
djhi Sep 19, 2024
69678c6
Apply suggestions from code review
djhi Sep 19, 2024
2b63cfe
Refactor useFirstResourceWithListAccess
djhi Sep 19, 2024
769cdfb
Reorganize Resource tests
djhi Sep 19, 2024
7323384
Improve ResourceMenuItem stories
djhi Sep 19, 2024
4d25054
Add default Unauthorized component to ra-ui-materialui
djhi Sep 19, 2024
484f728
Handle errors in canAccess hooks
djhi Sep 19, 2024
469ae82
Update authProvider documentation
djhi Sep 19, 2024
47e6836
Add missing export
djhi Sep 19, 2024
a5b0d45
Improve typings on useCanAccess & useCanAccessResources
djhi Sep 20, 2024
0b1252e
Add access control to views buttons
djhi Sep 20, 2024
6f13406
Pass the current record when available
djhi Sep 20, 2024
a789e19
Add documentation
djhi Sep 20, 2024
33fc627
Add CreateButton tests
djhi Sep 20, 2024
0c71f04
Improve canAccess hooks types
djhi Sep 20, 2024
e6e435e
Add EditButton tests
djhi Sep 20, 2024
c158c88
Add ShowButton tests
djhi Sep 20, 2024
9d730d6
Add ListButton tests
djhi Sep 20, 2024
c23e4f5
Add access control to DeleteButton
djhi Sep 20, 2024
a8be1f3
Fix other buttons stories
djhi Sep 20, 2024
afd578d
Add DeleteButton tests
djhi Sep 20, 2024
4472472
Apply suggestions from code review
djhi Sep 20, 2024
582b3de
Fix build
djhi Sep 23, 2024
fd77b62
Fix Unauthorized
djhi Sep 23, 2024
9cdc380
Fix useCanAccessResources documentation
djhi Sep 23, 2024
538c461
Fix CanAccess tests
djhi Sep 23, 2024
5861e9e
Fix types and exports
djhi Sep 23, 2024
97bdba1
Remove unnecessary fragment
djhi Sep 23, 2024
95485f0
Fix auth documentation
djhi Sep 23, 2024
76f32b2
Update Unauthorized design
djhi Sep 23, 2024
b40d39c
Apply suggestions from code review
djhi Sep 23, 2024
5c4251e
Update useCanAccess documentation
djhi Sep 23, 2024
82d33a0
Update useCanAccessResources documentation
djhi Sep 23, 2024
98336dc
Refactor canAccess hooks
djhi Sep 23, 2024
1d70577
Remove wrong prop
djhi Sep 23, 2024
5918515
Pessimistic authentication checks
djhi Sep 25, 2024
08258c9
Better design of authenticationError
djhi Sep 25, 2024
7d21e19
Update Admin documentation
djhi Sep 25, 2024
e6992b2
Fix default AuthenticationError and Unauthorized design and API
djhi Sep 25, 2024
680ee68
Update AuthenticationError documentation screenshot
djhi Sep 25, 2024
0a9ad7d
Simplify useListController auth check
djhi Sep 25, 2024
2cdea62
Fix useListController security story
djhi Sep 25, 2024
bff6f96
useListController security story design
djhi Sep 25, 2024
bb42e52
Avoid a rerender with useAuthState when no AuthProvider is set
djhi Sep 25, 2024
09505c4
Add authentication check to useEditController
djhi Sep 25, 2024
805a7c4
Add authentication check to useInfiniteListController
djhi Sep 25, 2024
b4c97c0
Add authentication check to useShowController
djhi Sep 25, 2024
21661bb
Simplify stories
djhi Sep 25, 2024
b2c828b
Fix typo in tests
djhi Sep 25, 2024
8455d7d
Improve tests
djhi Sep 25, 2024
b2f759b
Don't set logoutOnFailure to its default
djhi Sep 25, 2024
93099ef
Fix disableAuthentication
djhi Sep 25, 2024
2c8ce0b
Restore Resource access control
djhi Sep 25, 2024
1b84ccf
Apply suggestions from code review
fzaninotto Sep 26, 2024
87e65ba
Fix linter warnings
fzaninotto Sep 26, 2024
12d762e
Merge pull request #10238 from marmelab/pessimistic-authentication-ch…
fzaninotto Sep 26, 2024
98cf688
Introduce useRequireAccess
djhi Sep 27, 2024
ea857f0
Move access control from Resource to useShowController
djhi Sep 27, 2024
dace0e9
Move access control from Resource to useEditController
djhi Sep 27, 2024
eb61992
Move access control from Resource to useListController
djhi Sep 27, 2024
c02f540
Move access control from Resource to useCreateController
djhi Sep 27, 2024
079f4f5
Remove unnecessary contexts
djhi Sep 30, 2024
a78a2aa
Refactor CanAccess
djhi Sep 30, 2024
28d2b02
Add access control to useInfiniteController
djhi Sep 30, 2024
b1f0e00
Fix CanAccess js docs
djhi Sep 30, 2024
782b485
Avoid redirecting to /authentication-error in useCanAccess
djhi Sep 30, 2024
a552b5f
Remove old canAccess documentation
djhi Sep 30, 2024
97e85fa
Fix CanAccess documentation
djhi Sep 30, 2024
dafdc56
Update documentation
djhi Sep 30, 2024
f595820
Handle errors in <CanAccess>
djhi Sep 30, 2024
12f0777
Better useRequireAccess example
djhi Sep 30, 2024
009b852
Improve useRequireAccess tests
djhi Sep 30, 2024
de52132
Improve useRequireAccess jsDoc
djhi Sep 30, 2024
d3804d3
Revert unnecessary changes
djhi Sep 30, 2024
b4ca86b
Set the default unauthorized prop in ra-ui-materialui
djhi Sep 30, 2024
c429d46
Apply suggestions from code review
djhi Oct 1, 2024
53b24e8
Remove premium icons for non enterprise features
djhi Oct 1, 2024
766c5b5
Rename unauthorized to accessDenied
djhi Oct 1, 2024
a8adf6e
Make access control calls dependant on auth check calls
djhi Oct 1, 2024
36ec547
Rename image
djhi Oct 1, 2024
34b1c1c
Fix useRequireAccess documentation
djhi Oct 1, 2024
00e33cf
Simplify stories
djhi Oct 1, 2024
c0b26e0
Merge pull request #10247 from marmelab/access-control-controllers
fzaninotto Oct 1, 2024
4b04c41
Merge branch 'access-control-resources' into access-control-buttons
djhi Oct 1, 2024
a6df775
Improve stories names
djhi Oct 1, 2024
738b273
Merge branch 'access-control-buttons' into access-control-delete-buttons
djhi Oct 1, 2024
2b718fb
Add access control to `<Datagrid rowClick>`
djhi Sep 20, 2024
e238966
Use Record<string, any> instead of RaRecord
djhi Oct 1, 2024
b37cf6d
Merge branch 'access-control-buttons' into access-control-rowclick
djhi Oct 1, 2024
2419a2e
Fix useCanAccessCallback
djhi Oct 1, 2024
c84dff7
Merge pull request #10225 from marmelab/access-control-buttons
fzaninotto Oct 1, 2024
b569075
Merge pull request #10226 from marmelab/access-control-delete-buttons
fzaninotto Oct 1, 2024
9267276
[Doc] Rewrite access control documentation
fzaninotto Sep 30, 2024
a866f3a
Review
fzaninotto Oct 1, 2024
6556df4
Document access control in views
fzaninotto Oct 1, 2024
7ebb52f
Document controllers
fzaninotto Oct 1, 2024
c083814
Fix build
fzaninotto Oct 1, 2024
b2e2eeb
Add mention of built-in access control in action buttons
fzaninotto Oct 1, 2024
f2bc053
Merge branch 'next' into access-control-resources
fzaninotto Oct 1, 2024
d0172f9
Document buttons
fzaninotto Oct 1, 2024
38f2d17
Fix buttons doc
fzaninotto Oct 1, 2024
c88e259
Make Authenticated component secure by default
fzaninotto Oct 1, 2024
6b82a78
Fix useCanAccess result type
djhi Oct 2, 2024
8347890
Merge branch 'access-control-resources' into access-control-rowclick
djhi Oct 2, 2024
0053e0f
Update navigation and reference
djhi Oct 2, 2024
2202c9c
Apply suggestions from code review
fzaninotto Oct 2, 2024
259d6f0
Remove unnecessary canAccess calls in useGetPathForRecord
djhi Oct 2, 2024
6fb0f3a
Add tests and stories
djhi Oct 2, 2024
71f74d4
Add tests and stories for ReferenceField
djhi Oct 2, 2024
b5edf22
Merge pull request #10251 from marmelab/authenticated-pessimistic
djhi Oct 2, 2024
31934d7
Only check access rights for inferred link types
djhi Oct 2, 2024
bababb4
Revert unnecessary changes on useGetPathForRecord
djhi Oct 2, 2024
b5dabbf
Introduce `<NavigateToFirstResource>`
djhi Oct 2, 2024
c0549b4
Better formatting in documentation
djhi Oct 2, 2024
f789795
Add mention of authentication
fzaninotto Oct 3, 2024
1a43739
Merge pull request #10250 from marmelab/access-control-doc
fzaninotto Oct 3, 2024
e7c8541
Fix ShowBase should accept a ReactNode
djhi Oct 3, 2024
dfcbfe9
Reuse HintedString in useCreatePath
djhi Oct 3, 2024
10a1c76
Refactor useGetPathForRecord to leverage react-query
djhi Oct 3, 2024
439ab27
Add tests and stories for useGetPathForRecordCallback and improve can…
djhi Oct 3, 2024
3a05c9c
Improve ReferenceField tests and stories
djhi Oct 3, 2024
04fffab
Apply review suggestions
djhi Oct 3, 2024
fe49131
Merge branch 'access-control-first-resource' of github.com:marmelab/r…
djhi Oct 3, 2024
61b14c8
Export NavigateToFirstResource
djhi Oct 3, 2024
0481fb0
Reintroduce CreatePathType
djhi Oct 3, 2024
b7cfad2
Improve tests and stories
djhi Oct 3, 2024
2b76791
Add Datagrid story
djhi Oct 3, 2024
ed77a48
Add documentation
djhi Oct 3, 2024
2246a5d
Throw an error when no resources are found
djhi Oct 3, 2024
cffa9b4
Mibor tweaks
fzaninotto Oct 3, 2024
bdf0c99
Revert change on useGetPathForRecord and explain in comments
djhi Oct 3, 2024
8d02eda
Merge pull request #10227 from marmelab/access-control-rowclick
fzaninotto Oct 3, 2024
ad12e41
Remove the error
djhi Oct 3, 2024
d1be9bb
Merge pull request #10255 from marmelab/access-control-first-resource
fzaninotto Oct 3, 2024
44dc614
[Doc] Overhaul Auth introduction and auth provider writing chapters
fzaninotto Oct 3, 2024
4bdcf84
Reorganize auth doc
fzaninotto Oct 3, 2024
54b3522
Fix Admin authenticationError documentation
fzaninotto Oct 3, 2024
68c4066
Make authProvider.getPermissions optional
djhi Oct 4, 2024
c53bc86
Cleanup
djhi Oct 4, 2024
ec56f9f
Add story and screencast
fzaninotto Oct 4, 2024
86f1474
Merge pull request #10257 from marmelab/optional-authprovider-getperm…
fzaninotto Oct 4, 2024
40adf7d
Introduce useIsAuthPending
djhi Oct 4, 2024
18620e6
Ensure List check auth states pessimistically
djhi Oct 4, 2024
483d84c
Ensure InfiniteList check auth states pessimistically
djhi Oct 4, 2024
99ab061
Ensure Create check auth states pessimistically
djhi Oct 4, 2024
bb78db5
Ensure Edit check auth states pessimistically
djhi Oct 4, 2024
75c9ba5
Ensure Show check auth states pessimistically
djhi Oct 4, 2024
515c326
[Doc] Update requireAuth explanation
fzaninotto Oct 4, 2024
e6b2df3
[doc] Clarify that `getPermissions` is now optional
fzaninotto Oct 4, 2024
ecd4461
Proofreading
fzaninotto Oct 4, 2024
6076cf4
Ensure devs can provide their own loading
djhi Oct 4, 2024
41e9853
Ensure a ResourceContext is added only when needed
djhi Oct 4, 2024
abd28a2
Fix Resource documentation
fzaninotto Oct 4, 2024
d09d409
Fix missing export
fzaninotto Oct 4, 2024
ee520bb
Remove links to deleted chapters
fzaninotto Oct 4, 2024
581ac7a
Fix typo
fzaninotto Oct 4, 2024
842cd2f
Proofreading
fzaninotto Oct 4, 2024
5a2df6c
Add stories and tests for ListBase
djhi Oct 4, 2024
4e98cb6
Add stories and tests for InfiniteListBase
djhi Oct 4, 2024
fa587fd
Fix useAuthState is optimistic
fzaninotto Oct 4, 2024
68b92d5
Fix mention of optimistic default auth
fzaninotto Oct 4, 2024
8370f97
Add message to future me
fzaninotto Oct 4, 2024
5b4a06c
Update docs/CustomRoutes.md
fzaninotto Oct 4, 2024
6b984f1
Fix type
fzaninotto Oct 4, 2024
ff5b916
Fix useCanAccessResources should not log out on error
fzaninotto Oct 4, 2024
7008b31
Add stories and tests for Create
djhi Oct 4, 2024
34dfce2
Fix useCreateController isPending is always true if disableAuthentica…
fzaninotto Oct 4, 2024
0fee4fd
Add stories and tests for Edit
djhi Oct 4, 2024
3a6c3a8
Add stories and tests for Show
djhi Oct 4, 2024
8a8cead
Fix new auth error pages can't be overridden
fzaninotto Oct 4, 2024
a60d5e6
Ensure dashboard waits for all auth calls resolutions
djhi Oct 4, 2024
30924a4
Merge pull request #10258 from marmelab/access-control-views-loading
fzaninotto Oct 4, 2024
5e26e1f
Fix message key
fzaninotto Oct 4, 2024
452253d
Allow requireAuth to use react-query cache
djhi Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/Admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Here are all the props accepted by the component:
| `store` | Optional | `Store` | - | The Store for managing user preferences |
| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration |
| `title` | Optional | `string` | - | The error page title |
| `unauthorized` | Optional | `Component` | - | The component displayed in the `/unauthorized` page |
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved


## `dataProvider`
Expand Down Expand Up @@ -1005,6 +1006,30 @@ const MyTitle = () => {
};
```

## `unauthorized`

When using an authProvider that supports [the `canAccess` method](./AuthProviderWriting.md#canaccess), react-admin will check whether users can access a resource page and display the `unauthorized` component when they can't.

fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
You can replace that "unauthorized" screen by passing a custom component as the `unauthorized` prop:
djhi marked this conversation as resolved.
Show resolved Hide resolved

```tsx
import * as React from 'react';
import { Admin } from 'react-admin';

const Unauthorized = () => (
<div>
<h1>Authorization error</h1>
<p>You don't have access to this page.</p>
</div>
)

const App = () => (
<Admin unauthorized={Unauthorized}>
...
</Admin>
);
```

## Adding Custom Pages

The [`children`](#children) prop of the `<Admin>` component define the routes of the application.
Expand Down
39 changes: 39 additions & 0 deletions docs/AuthProviderWriting.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const authProvider = {
getIdentity: () => Promise.resolve(/* ... */),
handleCallback: () => Promise.resolve(/* ... */), // for third-party authentication only
// authorization
canAccess: params => Promise.resolve(/* ... */)
getPermissions: () => Promise.resolve(/* ... */),
};
```
Expand Down Expand Up @@ -65,6 +66,7 @@ const authProvider = {
fullName: 'John Doe',
}),
getPermissions: () => Promise.resolve(''),
canAccess: ({ action, resource, record }) => Promise.resolve(true),
};

export default authProvider;
Expand Down Expand Up @@ -402,6 +404,10 @@ React-admin doesn't use permissions by default, but it provides [the `usePermiss

[The Role-Based Access Control (RBAC) module](./AuthRBAC.md) allows fined-grained permissions in react-admin apps, and specifies a custom return format for `authProvider.getPermissions()`. Check [the RBAC documentation](./AuthRBAC.md#authprovider-methods) for more information.

### `canAccess`

This method should return a boolean indicating whether users can perform the provided action on the provided resource (in the [Role-Based Access Control (RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control) sense).
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

### `handleCallback`

This method is used when integrating a third-party authentication provider such as [Auth0](https://auth0.com/). React-admin provides a route at the `/auth-callback` path, to be used as the callback URL in the authentication service. After logging in using the authentication service, users will be redirected to this route. The `/auth-callback` route calls the `authProvider.handleCallback` method on mount.
Expand Down Expand Up @@ -475,6 +481,7 @@ React-admin calls the `authProvider` methods with the following params:
| `getIdentity` | Get the current user identity | |
| `handleCallback` | Validate users after third party authentication service redirection | |
| `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - empty for react-admin default routes |
| `canAccess` | Check authorization for an action over a resource | `{ action: string, resource: string, record: string }` |

## Response Format

Expand All @@ -489,6 +496,7 @@ React-admin calls the `authProvider` methods with the following params:
| `getIdentity` | Auth backend returned identity | `{ id: string | number, fullName?: string, avatar?: string }` |
| `handleCallback` | User is authenticated | `void | { redirectTo?: string | boolean }` route to redirect to after login |
| `getPermissions` | Auth backend returned permissions | `Object | Array` free format - the response will be returned when `usePermissions()` is called |
| `canAccess` | Auth backend returned authorization | `boolean |

## Error Format

Expand All @@ -503,4 +511,35 @@ When the auth backend returns an error, the Auth Provider should return a reject
| `getIdentity` | Auth backend failed to return identity | `Object` free format - returned as `error` when `useGetIdentity()` is called |
| `handleCallback` | Failed to authenticate users after redirection | `void | { redirectTo?: string, logoutOnFailure?: boolean, message?: string }` |
| `getPermissions` | Auth backend failed to return permissions | `Object` free format - returned as `error` when `usePermissions()` is called. The error will be passed to `checkError` |
| `canAccess` | Auth backend failed to return authorization | `Object` free format - returned as `error` when `useCanAccess()` is called. The error will be passed to `checkError` |

## Query Cancellation

React-admin supports [Query Cancellation](https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation), which means that when a component is unmounted, any pending query that it initiated is cancelled. This is useful to avoid out-of-date side effects and to prevent unnecessary network requests.

To enable this feature, your auth provider must have a `supportAbortSignal` property set to `true`.

```tsx
const authProvider = { /* ... */ };
authProvider.supportAbortSignal = true;
```

Now, every call to the auth provider will receive an additional `signal` parameter (an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance). You must pass this signal down to the fetch call:

```tsx
const authProvider = {
canAccess: async ({ resource, action, record, signal }) => {
const url = `${API_URL}/can_access?resource=${resource}&action=${action}`;
const options = { signal: params.signal };
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
const res = await fetch(url, options);
if (!res.ok) {
throw new HttpError(res.statusText);
}
return res.json();
},
}
```

Some auth providers may already support query cancellation. Check their documentation for details.

**Note**: In development, if your app is using [`<React.StrictMode>`](https://react.dev/reference/react/StrictMode), enabling query cancellation will duplicate the API queries. This is only a development issue and won't happen in production.
2 changes: 2 additions & 0 deletions docs/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const authProvider = {
getIdentity: () => Promise.resolve(),
// get the user permissions (optional)
getPermissions: () => Promise.resolve(),
// check whether users have the right to perform an action on a resource (optional)
canAccess: () => Promise.resolve(),
};
```

Expand Down
19 changes: 18 additions & 1 deletion docs/Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,21 @@ const App = () => (

When users navigate to the `/posts` route, react-admin will display a loading indicator while the `PostList` component is being loaded.

![Loading indicator](./img/lazy-resource.png)
![Loading indicator](./img/lazy-resource.png)

## Access Control

When using an authProvider that supports [the `canAccess` method](./AuthProviderWriting.md#canaccess), react-admin will check whether users can access a resource page and display [the `unauthorized` component](./Admin.md#unauthorized) when they can't.

For instance, given the following resource:

```tsx
<Resource name="posts" list={PostList} create={PostCreate} edit={PostEdit} show={PostShow} />
```

React-admin will call the `authProvider.canAccess` method when users try to access the pages with the following parameters:

- For the list page: `{ action: "list", resource: "posts" }`
- For the create page: `{ action: "create", resource: "posts" }`
- For the edit page: `{ action: "edit", resource: "posts" }`
- For the show page: `{ action: "show", resource: "posts" }`
37 changes: 16 additions & 21 deletions docs/useCanAccess.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,38 @@ title: "useCanAccess"

# `useCanAccess`

This hook, part of [the ra-rbac module](https://react-admin-ee.marmelab.com/documentation/ra-rbac)<img class="icon" src="./img/premium.svg" />, calls the `authProvider.getPermissions()` to get the role definitions, then checks whether the requested action and resource are allowed for the current user.
This hook calls the `authProvider.canAccess()` method on mount for a provided resource and action (and optionally a record). It returns an object containing a `canAccess` boolean set to `true` if users have access to the resource and action.
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

## Usage

`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.
`useCanAccess` takes an object `{ action, resource, record }` as argument. It returns an object describing the state of the request. As calls to the `authProvider` are asynchronous, the hook returns a `isPending` state in addition to the `canAccess` key.

```jsx
import { useCanAccess } from '@react-admin/ra-rbac';
import { useRecordContext, DeleteButton } from 'react-admin';
import { useCanAccess, useRecordContext, DeleteButton } from 'react-admin';

const DeleteUserButton = () => {
const record = useRecordContext();
const { isPending, canAccess } = useCanAccess({ action: 'delete', resource: 'users', record });
const { isPending, canAccess, error } = useCanAccess({ action: 'delete', resource: 'users', record });
if (isPending || !canAccess) return null;
if (error) return <div>{error.message}</div>
return <DeleteButton record={record} resource="users" />;
};
```

When checking if a user can access a resource, ra-rbac grabs the permissions corresponding to his roles. If at least one of these permissions allows him to access the resource, the user is granted access. Otherwise, the user is denied.

```jsx
const permissions = [
{ action: ["read", "create", "edit", "export"], resource: "companies" },
{ action: ["read", "create", "edit", "delete"], resource: "users" },
];
const authProvider= {
// ...
getPermissions: () => Promise.resolve({
permissions: [
{ 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" } },
],
}),
canAccess: ({ resource, action, record }) => {
const permission = permissions.find(p => {
if (p.resource !== resource) return false;
if (p.action.includes(action)) return false;
return true;
djhi marked this conversation as resolved.
Show resolved Hide resolved
})
},
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
};

const { canAccess: canUseCompanyResource } = useCanAccess({
Expand All @@ -56,10 +55,6 @@ const { canAccess: canReadSales } = useCanAccess({ action: "read", resource: "sa
const { canAccess: canReadSelfSales } = useCanAccess({ action: "read", resource: "sales" }, { id: "123" }); // canReadSelfSales is true
```

**Tip**: The *order* of permissions as returned by the `authProvider` isn't significant. As soon as at least one permission grants access to an action on a resource, the user will be able to perform it.

**Tip**: `useCanAccess` is asynchronous, because it calls `usePermissions` internally. If you have to use `useCanAccess` several times in a component, the rendered result will "blink" as the multiple calls to `authProvider.getPermissions()` resolve. To avoid that behavior, you can use the `usePermissions` hook once, then call [the `canAccess` helper](./canAccess.md).

## Parameters

`useCanAccess` expects a single parameter object with the following properties:
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './AuthContext';
export * from './LogoutOnMount';
export * from './types';
export * from './useAuthenticated';
export * from './useCanAccess';
export * from './useCheckAuth';
export * from './useGetIdentity';
export * from './useHandleAuthCallback';
Expand Down
124 changes: 124 additions & 0 deletions packages/ra-core/src/auth/useCanAccess.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as React from 'react';
import expect from 'expect';
import { waitFor, render, screen } from '@testing-library/react';

import { QueryClient } from '@tanstack/react-query';
import { Basic } from './useCanAccess.stories';

describe('useCanAccess', () => {
it('should return a loading state on mount', () => {
render(<Basic />);
screen.getByText('LOADING');
});

it('should return isPending: true by default after a tick', async () => {
render(<Basic />);
screen.getByText('LOADING');
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
});
});

it('should allow access on mount when there is no authProvider', () => {
render(<Basic authProvider={null} />);
expect(screen.queryByText('LOADING')).toBeNull();
screen.getByText('canAccess: YES');
});

it('should return that the resource is accessible when canAccess return true', async () => {
const authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
getPermissions: () => Promise.reject('bad method'),
canAccess: () => Promise.resolve(true),
};
render(<Basic authProvider={authProvider} />);
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('canAccess: YES')).not.toBeNull();
});
});

it('should return that the resource is accessible when auth provider does not have an canAccess method', async () => {
const authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
getPermissions: () => Promise.reject('bad method'),
canAccess: undefined,
};
render(<Basic authProvider={authProvider} />);

await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('canAccess: YES')).not.toBeNull();
});
});

it('should return that the resource is not accessible when canAccess return false', async () => {
const authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
getPermissions: () => Promise.reject('bad method'),
canAccess: () => Promise.resolve(false),
};
render(<Basic authProvider={authProvider} />);

await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('canAccess: NO')).not.toBeNull();
expect(screen.queryByText('ERROR')).toBeNull();
});
});

it('should return an error after a tick if the auth.canAccess call fails and checkError resolves', async () => {
const authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
getPermissions: () => Promise.reject('bad method'),
checkError: () => Promise.resolve(),
canAccess: () => Promise.reject('not good'),
};
render(<Basic authProvider={authProvider} />);

await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
});
await waitFor(() => {
expect(screen.queryByText('ERROR')).not.toBeNull();
});
});

it('should abort the request if the query is canceled', async () => {
const abort = jest.fn();
const authProvider = {
canAccess: jest.fn(
({ signal }) =>
new Promise(() => {
signal.addEventListener('abort', () => {
abort(signal.reason);
});
})
) as any,
checkError: () => Promise.resolve(),
supportAbortSignal: true,
} as any;
const queryClient = new QueryClient();
render(<Basic authProvider={authProvider} queryClient={queryClient} />);
await waitFor(() => {
expect(authProvider.canAccess).toHaveBeenCalled();
});
queryClient.cancelQueries({
queryKey: ['auth', 'canAccess'],
});
await waitFor(() => {
expect(abort).toHaveBeenCalled();
});
});
});
Loading
Loading