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
authClient
in every files where you use theaor-permissions
components and specify it in theirauthClient
prop, which is fine if you don't have many, or - 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 theauthClient
call resolved to a single value too, we check whether they are equals. - If
value
is a single value and theauthClient
call resolved to an array of values, we check whether this array contains the required value. - If both
value
and the result of theauthClient
call are arrays, we need to know if an exact match is required: eg bothvalue
and the result of theauthClient
call must have the same set of items or only share the values specified in thevalue
prop. That's the purpose of theexact
prop (which isfalse
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 thevalue
propexact
: the value of theexact
proprecord
: 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!