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:
- You can import your
authClientin every files where you use theaor-permissionscomponents and specify it in theirauthClientprop, which is fine if you don’t have many, or - You can use the
AuthProvidercomponent 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
valueis a single value and theauthClientcall resolved to a single value too, we check whether they are equals. - If
valueis a single value and theauthClientcall resolved to an array of values, we check whether this array contains the required value. - If both
valueand the result of theauthClientcall are arrays, we need to know if an exact match is required: eg bothvalueand the result of theauthClientcall must have the same set of items or only share the values specified in thevalueprop. That’s the purpose of theexactprop (which isfalseby 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 thevaluepropexact: the value of theexactproprecord: 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!
Authors
Full-stack web developer at marmelab, Gildas has a strong appetite for emerging technologies. If you want an informed opinion on a new library, ask him, he's probably used it on a real project already.