Web Applications become more and more complex thanks to the power and capabilities of modern browsers. With bigger and broader applications rises the need of authentication and segmentation of users.
In fact, different users have different rights on the application, allowing them to perform different actions according to their profile/role.
In this article we will learn how to organise the permissions and how to display part of your application based on the permissions given to the current user.
TL;DR;
If you are more of the show-me-the-code type of developer, you can find the final code here: https://github.com/francois-roget/permission-provider-demo
This is not security!
I would like to stress that this mechanism is not intended to prevent unauthorised requests to the server. It is more of a user experience improvement.
We have to keep in mind that any Javascript code running on the browser is present and completely readable by the end user. Hiding a button or a screen with this technique will not prevent anybody to look at the code and discover the API endpoints or the internal logic of the application.
All calls made to the API should be checked, on the server side, for user’s permissions before taking any action.
How to organise your permissions
The more naïve/tempting way to deal with user permission is to check, for example, if the current user is “admin”. Based on this, different parts of the application would be allowed or not.
Doing this will quickly block you as new requests will certainly arrive to change what a (non-)admin user can see or to create another type of user that can do more than a simple user but less than an admin.
The best way to deal with permissions is to create one permission per granular action that would be allowed or restricted in the application. For example, you can define separated permissions for:
- Viewing a list of elements (
element.list
) - Adding an element (
element.add
) - Modifying an element (
element.edit
) - Removing an element (
element.delete
)
The idea would be then to aggregate those permissions into roles where you define what users assigned to that role can do. For example, creating a role “admin” with all the permissions would be a good starting point. You could then create a role “viewer” with only element.list
as permissions and a role “contributor” with element.list
and element.add
.
Simple Use Case
Let’s image a simple CRUD use case where the logged user has different actions available according to their roles:
- The Viewer role can only list the items
- The Contributor role can list items and add new ones
- The Administrator role can do all actions (list items, add an item and delete an item)
Minimal Viable Product
Now let’s see the minimal code that we need to implement in order to have the required features.
First of all we will use the React’s Context API. This api allows the developer to create a Context that is composed of 2 elements:
- A Provider that will hold the data
- A Consumer that will consume the data provided by the Provider
In this case, the data passed between the provider and the consumer is a method allowing the consumer to know if a particular permission is granted to the current user.
import React from 'react';
import {Permission} from "../Types";
type PermissionContextType = {
isAllowedTo: (permission: Permission) => boolean;
}
// Default behaviour for the Permission Provider Context
// i.e. if for whatever reason the consumer is used outside of a
provider
// The permission will not be granted if no provider says otherwise
const defaultBehaviour: PermissionContextType = {
isAllowedTo: () => false
}
// Create the context
const PermissionContext = React.createContext<PermissionContextType>(defaultBehaviour);
export default PermissionContext;
Next we should implement the PermissionProvider that will hold the logic of checking the user permission. This PermissionProvider will receive the user’s permissions as a prop and provide the implementation of the method isAllowedTo
.
import React from 'react';
import {Permission} from "../Types";
import PermissionContext from "./PermissionContext";
type Props = {
permissions: Permission[]
}
// This provider is intended to be surrounding the whole
application.
// It should receive the users permissions as parameter
const PermissionProvider: React.FunctionComponent<Props> = ({permissions, children}) => {
// Creates a method that returns whether the requested
permission is available in the list of permissions
// passed as parameter
const isAllowedTo = (permission: Permission) =>
permissions.includes(permission);
// This component will render its children wrapped around a
PermissionContext's provider whose
// value is set to the method defined above
return <PermissionContext.Provider value={{isAllowedTo}}
>{children}</PermissionContext.Provider>;
};
export default PermissionProvider;
The last component we need is a consumer to use inside our application at every place we need to conditionally render part of the UI.
import React, {useContext} from 'react';
import PermissionContext from "./PermissionContext"; import {Permission} from "../Types";
type Props = {
to: Permission;
};
// This component is meant to be used everywhere a restriction based
on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, children}) => {
// We "connect" to the provider thanks to the PermissionContext
const {isAllowedTo} = useContext(PermissionContext);
// If the user has that permission, render the children
if(isAllowedTo(to)){
return <>{children}</>;
}
// Otherwise, do not render anything
return null;
};
export default Restricted;
Using these components, we can surround our application with the PermissionProvider, then use the Restricted component.
<PermissionProvider permissions={currentUser.permissions}> ...
<Restricted to="list.elements">
<ElementList elements={elements} addElement={addElement}
removeElement={removeElement}/>
</Restricted>
</PermissionProvider>
<div className="container">
<table className="table table-sm table-hover">
<thead className="thead-light">
<tr>
<th scope="col">Name</th>
<th scope="col">Price</th>
<th scope="col">Currency</th>
<th scope="col" className="text-right">
<Restricted to='add.element'>
<button className="btn btn-primary btn-sm"
onClick={addRandomElement}>
<i className="bi-plus-circle"/>
</button>
</Restricted>
</th> </tr>
</thead>
<tbody>
{elements.map(e => (
<tr key={e.name}>
<td>{e.name}</td>
<td>{e.price}</td>
<td>{e.currency}</td>
<td className="text-right">
<Restricted to='delete.element'>
<button className="btn btn-danger btn-sm"
onClick={() => removeElement(e)}>
<i className="bi bi-trash"/>
</button>
</Restricted>
</td> </tr>
))}
</tbody>
</table>
</div>
This allows displaying the action button only to the users having the right permission.
Enhancements
Fallback renderer
In order to provide more flexibility to the developers and UI designers, it would be great to have the possibility to display an alternative UI in case the user does not have a particular permission. For this, a fallback property will be added to the Restriction component.
import React, {useContext} from 'react';
import PermissionContext from "./PermissionContext"; import {Permission} from "../Types";
type Props = {
to: Permission;
fallback?: JSX.Element | string;
};
// This component is meant to be used everywhere a restriction based
on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, children}) => {
// We "connect" to the provider thanks to the PermissionContext
const {isAllowedTo} = useContext(PermissionContext);
// If the user has that permission, render the children
if(isAllowedTo(to)){
return <>{children}</>;
}
// Otherwise, render the fallback
return <>{fallback}</>;
};
export default Restricted;
Custom hook
Creating a custom hook will allow the usage of the permission in more complex situations. This can be useful when we need to have a custom logic (not only rendering) based on the user’s permission.
import {useContext} from 'react';
import PermissionContext from "./PermissionContext"; import {Permission} from "../Types";
const usePermission = (permission: Permission) => { const {isAllowedTo} = useContext(PermissionContext); return isAllowedTo(permission);
}
export default usePermission;
This custom hook can now be used in the Restricted component.
import React from 'react';
import {Permission} from "../Types";
import usePermission from "./usePermission";
type Props = {
to: Permission;
fallback?: JSX.Element | string;
};
// This component is meant to be used everywhere a restriction based
on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, children}) => {
// We "connect" to the provider thanks to the permission hook
9
const allowed = usePermission(to);
// If the user has that permission, render the children
if(allowed){
return <>{children}</>;
}
// Otherwise, render the fallback
return <>{fallback}</>;
};
export default Restricted;
Going Further
Now let’s consider a more complex (real-world) use case where the users have a lot of permissions and that those permissions cannot be fetched at login time, or the permissions can only be fetched by domain area.
In such cases, the fetching and checking of the permission is asynchronous. This delay has to be taken into account at provider and at consumer levels.
In order to make it asynchronous, we need to update the provider to return a Promise from the isAllowedTo method. In the case of async permission fetching, the PermissionProvider now receives an “async method to get a permission” instead of a “list of permissions”.
At the same time, the requested permissions can be cached at PermissionProvider level in order to speed up the UI.
// This provider is intended to be surrounding the whole
application.
// It should receive a method to fetch permissions as parameter
const PermissionProvider: React.FunctionComponent<Props> = ({fetchPermission, children}) => {
const cache: PermissionCache = {};
// Creates a method that returns whether the requested
permission is granted to the current user
const isAllowedTo = async (permission: Permission):
Promise<boolean> => {
if(Object.keys(cache).includes(permission)){
return cache[permission];
}
const isAllowed = await fetchPermission(permission);
cache[permission] = isAllowed;
return isAllowed;
};
// This component will render its children wrapped around
// a PermissionContext's provider whose
// value is set to the method defined above
return <PermissionContext.Provider value={{isAllowedTo}}
>{children}</PermissionContext.Provider>;
};
At the same time, the consumer should await for the promise to complete. The question is “what should it display while the permission is fetched?”. The consumer can either render nothing or a loading component passed as parameter.
type Props = {
to: Permission;
fallback?: JSX.Element | string;
loadingComponent?: JSX.Element | string;
};
// This component is meant to be used everywhere a restriction based
on user permission is needed
const Restricted: React.FunctionComponent<Props> = ({to, fallback, loadingComponent, children}) => {
// We "connect" to the provider thanks to the PermissionContext
const [loading, allowed] = usePermission(to);
if(loading){
return <>{loadingComponent}</>;
}
// If the user has that permission, render the children
if(allowed){
return <>{children}</>;
}
// Otherwise, render the fallback
return <>{fallback}</>;
};
That’s It Folks
I really hope this article will help you design more user friendly user interfaces. You can find all the code here: https://github.com/francois-roget/permission- provider-demo (take a look at the tags for the different stages).
Top comments (0)