Marmelab Blog

Introducing aor-permissions: Customize Admin-on-rest based on user permissions

Admin interfaces are often multi-user, with custom permissions or roles per user.

Defining what a user has access to or what they can actually do might be implemented differently depending on your application complexity and needs: you might have a single role for each user (admin, manager, etc.), a list of roles, or an Access Control List (ACL).

As this is a fairly common usecase, we decided to offer a well integrated solution for it in admin-on-rest, the frontend framework for building admin SPAs on top of REST services, using React and Material Design.

Introducing aor-permissions

aor-permissions is an add-on for admin-on-rest. It simply displays or hides children components based on the current user permission, for instance using the <WithPermission> component:

const PostEdit = ({ ...props }) => (
    <Edit {...props}>
        <SimpleForm>
            <DisabledInput source="id" />
            <WithPermission value="supereditor">
                <TextInput source="title" />
            </WithPermission>
            <TextInput source="body" />
        </SimpleForm>
    </Edit>
);

Installation

You can install it with npm:

npm install --save aor-permissions

or

yarn add aor-permissions

Now, the first thing to do is to update the authClient to support a new type provided by aor-permissions: AUTH_GET_PERMISSIONS. Here is a trivial example using a JSONWebToken:

// in authClient.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from 'admin-on-rest';
import { AUTH_GET_PERMISSIONS } from 'aor-permissions';
import { decode } from 'jsonwebtoken';

export default (type, params) => {
    // to login, fetch token and role from auth server, and store them in local storage
    if (type === AUTH_LOGIN) {
        const { username, password } = params;
        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(({ token }) => {
                const decoded = decode(token);
                localStorage.setItem('token', token);
                localStorage.setItem('role', decoded.role);
            });
    }
    // ... usual authClient code

    // now simply read permissions from local storage
    if (type === AUTH_GET_PERMISSIONS) {
        return Promise.resolve(localStorage.getItem('role'));
    }
};

Note that we return a promise as we do with other types. This promise must resolve to a permission. As stated above, permissions can be a single role, an array of roles or an ACL. It might even be your user model directly.

Permission Components

aor-permissions introduces two React components allowing to customize the UI depending on your user permissions: WithPermission, and SwitchPermissions.

Bot these components need access to the authClient. There are two options to allow this:

  1. You can import your authClient in every files where you use the aor-permissions components and specify it in their authClient prop, which is fine if you don't have many, or
  2. You can use the AuthProvider component as a wrapper to your application:
import { Admin, Resource } from 'admin-on-rest';
import { AuthProvider } from 'aor-permissions';
import authClient from './authClient';

export const App = () => (
    <AuthProvider authClient={authClient}>
        <Admin>
            {/* Usual Resource components */}
        </Admin>
    </AuthProvider>
)

Notice we don't need to specify the authClient prop on the Admin component as it will be passed by the AuthProvider component. From now on, the authClient is available in the react context.

Permission Check

WithPermission and Permission both accept a value prop, which ca either be a single permission, or an array of permissions. Let's see how theses values are matched with the permissions returned by the authClient:

  • If value is a single value and the authClient call resolved to a single value too, we check whether they are equals.
  • If value is a single value and the authClient call resolved to an array of values, we check whether this array contains the required value.
  • If both value and the result of the authClient call are arrays, we need to know if an exact match is required: eg both value and the result of the authClient call must have the same set of items or only share the values specified in the value prop. That's the purpose of the exact prop (which is false by default).

Sometimes, you may need more complex constraints checks. Let's say you don't use roles or ACL at all, and only have one or several booleans or enumerations directly on your user model. You'll have to use the resolve prop.

resolve accepts a function which must returns true or false (or a promise resolving to true or false). It will receive an object with the following properties:

  • permissions: the result of the authClient call (could be your user model).
  • resource: the resource being checked (eg: products, posts, etc.)
  • value: the value of the value prop
  • exact: the value of the exact prop
  • record: Only when inside an Edit component, the record being edited

A simple implementation could be:

// in permissions.js
export const hasSuperPowers = (parameters) => {
    return parameters.permissions.hasSuperPowers;
}

WithPermission

Use WithPermission to show a component only when the user permissions match the specified permissions.

For example, let's say only the administrators can see the key and the secret for a configurable API in an edition view:

// in src/configurations.js
import React from 'react';
import { Edit, DisabledInput, LongTextInput, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'admin-on-rest';
import { WithPermission } from 'aor-permissions';

export const ConfigurationEdit = (props) => (
    <Edit {...props}>
        <SimpleForm>
            <DisabledInput source="id" />
            <TextInput source="title" />
            <TextInput source="endPoint" />
            <WithPermission value="administrator">
                <TextInput source="apiKey" />
                <TextInput source="apiSecret" />
            </WithPermission>
        </SimpleForm>
    </Edit>
);

WithPermission is not limited to inputs, it can also be used to hide a menu item in a custom menu:

// in customMenu.js
import React from 'react';
import MenuItem from 'material-ui/MenuItem';
import SettingsIcon from 'material-ui/svg-icons/action/settings';
import { WithPermission } from 'aor-permissions';
import authClient from './authClient';

const Menu = ({ onMenuTap, logout }) => (
    <div>
        {/* Other menu items */}

        <WithPermission value="administrator">
            <MenuItem
                containerElement={<Link to="/configuration" />}
                primaryText="Configuration"
                leftIcon={<SettingsIcon />}
                onTouchTap={onMenuTap}
            />
        </WithPermission>
        {logout}
    </div>
);

export default Menu;

While trying to match permissions, it will display a loading component with the Material-UI LinearProgress component by default. You may override it by specifying the loading prop.

SwitchPermissions

Use SwitchPermissions to display different contents for different users, be it views, fields, inputs, menu items, etc. To define the different cases, it accepts Permission children:

// In products.js
import { SwitchPermissions, Permission } from 'aor-permissions';
import authClient from '../authClient';
import { hasSuperPowers } from '../permissions';

// ...other views as usual (List, Create, etc.)

// Use this ProductEdit component as usual in your resource declaration
export const ProductEdit = props => (
    <SwitchPermissions {...props}>
        <Permission value="role1">
            <Edit {...props}>
                {/* Usual layout component */}
            </Edit>
        </Permission>
        <Permission value={['role2', 'role3']}>
            <Edit {...props}>
                {/* Usual layout component */}
            </Edit>
        </Permission>
        <Permission value={['role2', 'role3']} exact>
            <Edit {...props}>
                {/* Usual layout component */}
            </Edit>
        </Permission>
        <Permission resolve={hasSuperPowers} exact>
            <Edit {...props}>
                {/* Usual layout component */}
            </Edit>
        </Permission>
    </SwitchPermissions>
);

The Permission component accepts the exact same props as the WithPermission component.

If none of the declared permissions are matched, the SwitchComponent will render an empty node, but you may specify a notFound prop with the component to display if you want.

While trying to match permissions, it will display a loading component with the Material-UI LinearProgress component by default. You may override it by specifying the loading prop.

Conclusion

This project is open source. Please use it and give us feedbacks!