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

[RFR] Add auth hooks #3368

Merged
merged 18 commits into from
Jul 1, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
36 changes: 36 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,39 @@ export const UserCreate = (props) => (
</Create>
);
```

## `authProvider` No Longer Uses Legacy React Context

When you provide an `authProvider` to the `<Admin>` component, react-admin creates a context to make it available everywhere in the application. In version 2.x, this used the [legacy React context API](https://reactjs.org/docs/legacy-context.html). In 3.0, this uses the normal context API. That means that any context consumer will need to use the new context API.

```diff
-import React from 'react';
+import React, { useContext } from 'react';
+import { AuthContext } from 'react-admin';

-const MyComponentWithAuthProvider = (props, context) => {
+const MyComponentWithAuthProvider = (props) => {
+ const authProvider = useContext(AuthContext);
authProvider('AUTH_CHECK');
return <div>I'm authenticated</div>;
}

-MyComponentWithAuthProvider.contextTypes = { authProvider: PropTypes.object }
```

If you didn't access the `authProvider` context manually, you have nothing to change. All react-admin components have been updated to use the new context API.

Note that direct access to the `authProvider` from the context is discouraged (and not documented). If you need to interact with the `authProvider`, use the new `useAuth()` and `usePermissions()` hooks, or the auth-related action creators (`userLogin`, `userLogout`, `userCheck`).

## `authProvider` No Longer Receives `match` in Params

Whenever it called the `authProvider`, react-admin used to pass both the `location` and the `match` object from react-router. In v3, the `match` object is no longer passed as argument. There is no legitimate usage of this parameter we can think about, and it forced passing down that object across several components for nothing, so it's been removed. Upgrade your `authProvider` to remove that param.

```diff
// in src/authProvider
export default (type, params) => {
- const { location, match } = params;
+ const { location } = params;
// ...
}
```
245 changes: 151 additions & 94 deletions docs/Authentication.md

Large diffs are not rendered by default.

176 changes: 67 additions & 109 deletions docs/Authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ title: "Authorization"

# Authorization

Some applications may require to determine what level of access a particular authenticated user should have to secured resources. Since there are many different possible strategies (single role, multiple roles or rights, etc.), react-admin simply provides hooks to execute your own authorization code.
Some applications may require fine grained permissions to enable or disable access to certain features. Since there are many different possible strategies (single role, multiple roles or rights, ACLs, etc.), react-admin simply provides hooks to execute your own authorization code.

By default, a react-admin app doesn't require authorization. However, if needed, it will rely on the `authProvider` introduced in the [Authentication](./Authentication.md) section.
By default, a react-admin app doesn't check authorization. However, if needed, it will rely on the `authProvider` introduced in the [Authentication documentation](./Authentication.md) to do so. You should read that chapter first.

## Configuring the Auth Provider

A call to the `authProvider` with the `AUTH_GET_PERMISSIONS` type will be made each time a component requires to check the user's permissions.
Each time react-admin needs to determine the user permissions, it calls the `authProvider` with the `AUTH_GET_PERMISSIONS` type. It's up to you to return the user permissions, be it a string (e.g. `'admin'`) or and array of roles (e.g. `['post_editor', 'comment_moderator', 'super_admin']`).

Following is an example where the `authProvider` stores the user's role upon authentication, and returns it when called for a permissions check:
Following is an example where the `authProvider` stores the user's permissions in `localStorage` upon authentication, and returns these permissions when called with `AUTH_GET_PERMISSIONS`:

{% raw %}
```jsx
Expand All @@ -39,12 +39,12 @@ export default (type, params) => {
.then(({ token }) => {
const decodedToken = decodeJwt(token);
localStorage.setItem('token', token);
localStorage.setItem('role', decodedToken.role);
localStorage.setItem('permissions', decodedToken.permissions);
});
}
if (type === AUTH_LOGOUT) {
localStorage.removeItem('token');
localStorage.removeItem('role');
localStorage.removeItem('permissions');
return Promise.resolve();
}
if (type === AUTH_ERROR) {
Expand All @@ -54,7 +54,7 @@ export default (type, params) => {
return localStorage.getItem('token') ? Promise.resolve() : Promise.reject();
}
if (type === AUTH_GET_PERMISSIONS) {
const role = localStorage.getItem('role');
const role = localStorage.getItem('permissions');
return role ? Promise.resolve(role) : Promise.reject();
}
return Promise.reject('Unknown method');
Expand All @@ -64,9 +64,8 @@ export default (type, params) => {

## Restricting Access to Resources or Views

It's possible to restrict access to resources or their views inside the `Admin` component. To do so, you must specify a function as the `Admin` only child. This function will be called with the permissions returned by the `authProvider`.
Permissions can be useful to to restrict access to resources or their views. To do so, you must use a function as the `<Admin>` only child. React-admin will call this function with the permissions returned by the `authProvider`.

{% raw %}
```jsx
<Admin
dataProvider={dataProvider}
Expand All @@ -87,42 +86,22 @@ It's possible to restrict access to resources or their views inside the `Admin`
]}
</Admin>
```
{% endraw %}

Note that the function returns an array of React elements. This is required to avoid having to wrap them in a container element which would prevent the `Admin` from working.

**Tip** Even if that's possible, be careful when completely excluding a resource (like with the `categories` resource in this example) as it will prevent you to reference them in the other resource views, too.
**Tip**: Even if that's possible, be careful when completely excluding a resource (like with the `categories` resource in this example) as it will prevent you to reference this resource in the other resource views, too.

## Restricting Access to Fields and Inputs

You might want to display some fields or inputs only to users with specific permissions. Those permissions are retrieved for each route and will provided to your component as a `permissions` prop.

Each route will call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and some parameters including the current location and route parameters. It's up to you to return whatever you need to check inside your component such as the user's role, etc.
You might want to display some fields or inputs only to users with specific permissions. By default, react-admin calls the `authProvider` for permissions for each resource routes, and passes them to the `list`, `edit`, `create`, and `show` components.

Here's an example inside a `Create` view with a `SimpleForm` and a custom `Toolbar`:
Here is an example of a `Create` view with a conditionnal Input based on permissions:

{% raw %}
```jsx
const UserCreateToolbar = ({ permissions, ...props }) =>
<Toolbar {...props}>
<SaveButton
label="user.action.save_and_show"
redirect="show"
submitOnEnter={true}
/>
{permissions === 'admin' &&
<SaveButton
label="user.action.save_and_add"
redirect={false}
submitOnEnter={false}
variant="text"
/>}
</Toolbar>;

export const UserCreate = ({ permissions, ...props }) =>
<Create {...props}>
<SimpleForm
toolbar={<UserCreateToolbar permissions={permissions} {...props} />}
defaultValue={{ role: 'user' }}
>
<TextInput source="name" validate={[required()]} />
Expand All @@ -133,9 +112,7 @@ export const UserCreate = ({ permissions, ...props }) =>
```
{% endraw %}

**Tip** Note how the `permissions` prop is passed down to the custom `toolbar` component.

This also works inside an `Edition` view with a `TabbedForm`, and you can hide a `FormTab` completely:
This also works inside an `Edition` view with a `TabbedForm`, and you can even hide a `FormTab` completely:

{% raw %}
```jsx
Expand All @@ -155,9 +132,8 @@ export const UserEdit = ({ permissions, ...props }) =>
```
{% endraw %}

What about the `List` view, the `DataGrid`, `SimpleList` and `Filter` components? It works there, too.
What about the `List` view, the `DataGrid`, `SimpleList` and `Filter` components? It works there, too. And in the next example, the `permissions` prop is passed down to a custom `filters` component.

{% raw %}
```jsx
const UserFilter = ({ permissions, ...props }) =>
<Filter {...props}>
Expand All @@ -167,44 +143,30 @@ const UserFilter = ({ permissions, ...props }) =>
alwaysOn
/>
<TextInput source="name" />
{permissions === 'admin' ? <TextInput source="role" /> : null}
{permissions === 'admin' && <TextInput source="role" />}
</Filter>;

export const UserList = ({ permissions, ...props }) =>
<List
{...props}
filters={props => <UserFilter permissions={permissions} {...props} />}
sort={{ field: 'name', order: 'ASC' }}
>
<Responsive
small={
<SimpleList
primaryText={record => record.name}
secondaryText={record =>
permissions === 'admin' ? record.role : null}
/>
}
medium={
<Datagrid>
<TextField source="id" />
<TextField source="name" />
{permissions === 'admin' && <TextField source="role" />}
{permissions === 'admin' && <EditButton />}
<ShowButton />
</Datagrid>
}
/>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
{permissions === 'admin' && <TextField source="role" />}
{permissions === 'admin' && <EditButton />}
<ShowButton />
</Datagrid>
</List>;
```
{% endraw %}

**Tip** Note how the `permissions` prop is passed down to the custom `filters` component.
**Tip**: When calling the `authProvider` with the `AUTH_GET_PERMISSIONS` type, react-admin passes the current location. You can use this information to implement location-based authorization.

## Restricting Access to Content Inside a Dashboard
## Restricting Access to the Dashboard

The component provided as a [`dashboard`]('./Admin.md#dashboard) will receive the permissions in its props too:
React-admin injects the permissions into the component provided as a [`dashboard`]('./Admin.md#dashboard), too:

{% raw %}
```jsx
// in src/Dashboard.js
import React from 'react';
Expand All @@ -223,83 +185,79 @@ export default ({ permissions }) => (
</Card>
);
```
{% endraw %}

## Restricting Access to Content Inside Custom Pages
## `usePermissions()` Hook

You might want to check user permissions inside a [custom pages](./Admin.md#customroutes). You'll have to use the `WithPermissions` component for that. It will ensure the user is authenticated then call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and the `authParams` you specify:
You might want to check user permissions inside a [custom page](./Admin.md#customroutes). That's the purpose of the `usePermissions()` hook,which calls the `authProvider` with the `AUTH_GT_PERMISSIONS` type on mount, and returns the result when available:
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

{% raw %}
```jsx
// in src/MyPage.js
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { Title, WithPermissions } from 'react-admin';
import { withRouter } from 'react-router-dom';
import { usePermissions } from 'react-admin';

const MyPage = ({ permissions }) => (
<Card>
<Title title="My custom page" />
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
{permissions === 'admin'
? <CardContent>Sensitive data</CardContent>
: null
}
</Card>
)
const MyPageWithPermissions = ({ location, match }) => (
<WithPermissions
authParams={{ key: match.path, params: route.params }}
// location is not required but it will trigger a new permissions check if specified when it changes
location={location}
render={({ permissions }) => <MyPage permissions={permissions} /> }
/>
);
const MyPage = () => {
const { permissions } = usePermissions();
return (
<Card>
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
{permissions === 'admin' &&
<CardContent>Sensitive data</CardContent>
}
</Card>
);
}

export default MyPageWithPermissions;
export default MyPage;

// in src/customRoutes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Foo from './Foo';
import Bar from './Bar';
import Baz from './Baz';
import MyPageWithPermissions from './MyPage';
import MyPage from './MyPage';

export default [
<Route exact path="/foo" component={Foo} />,
<Route exact path="/bar" component={Bar} />,
<Route exact path="/baz" component={Baz} noLayout />,
<Route exact path="/baz" component={MyPageWithPermissions} />,
<Route exact path="/baz" component={MyPage} />,
];
```
{% endraw %}

## Restricting Access to Content in Custom Menu
The `usePermissions` hook is optimistic: it doesn't block rendering during the `authProvider` call. In the above example, the `MyPage` component renders even before getting the response from the `authProvider`. To avoid a blink in the interface while the `authProvider` is answering, use the `loaded` return value of `usePermissions()`:

What if you want to check the permissions inside a [custom menu](./Admin.md#menu) ? Much like getting permissions inside a custom page, you'll have to use the `WithPermissions` component:
```jsx
const MyPage = () => {
const { loaded, permissions } = usePermissions();
return loaded ? (
<Card>
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
{permissions === 'admin' &&
<CardContent>Sensitive data</CardContent>
}
</Card>
) : null;
}
```

## Restricting Access to a Menu

What if you want to check the permissions inside a [custom menu](./Admin.md#menu)? Much like getting permissions inside a custom page, you'll have to use the `usePermissions` hook:

{% raw %}
```jsx
// in src/myMenu.js
import React from 'react';
import { connect } from 'react-redux';
import { MenuItemLink, WithPermissions } from 'react-admin';
import { MenuItemLink, usePermissions } from 'react-admin';

const Menu = ({ onMenuClick, logout }) => (
const Menu = ({ onMenuClick, logout }) => {
const { permissions } = usePermissions();
return (
<div>
<MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} />
<MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} />
<WithPermissions
render={({ permissions }) => (
permissions === 'admin'
? <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
: null
)}
/>
{ permissions === 'admin' &&
<MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
}
{logout}
</div>
);
}
```
{% endraw %}
Loading