DEV Community

Sudhanshu
Sudhanshu

Posted on • Edited on

6 1

Role based access control in React-Redux apps

This assumes you have a basic knowledge of react, react-router, redux and redux-middlewares. Also, you have already implemented authentication in the react-redux app.

Note: I have used redux but the same can be done without relying on redux. So, while some knowledge of redux may help you understand what I have tried to convey, redux isn't a necessity.


While working in a project, the requirement arose to give the functionality of Role based access control (RBAC) in a client-side-rendered react-app.

Basically, the application was going to have multiple tiers of users with flexible permissions and depending on the permission granted to any particular user, a component or a part of the application may/may not become accessible to that user.

Implementing RBAC at the client-side can be divided into:

  • Conditional rendering of the components
  • Updating the conditions when permissions change

Conditional Rendering of the components:

There are many approaches that can be used for this. One of my initial approach was to wrap the component (say CheckedComponent) with a component (say CheckPermissions) which would conditionally render them based on the permissions given to the current user. This looks something like this:

import React, { useState } from "react"
import { connect } from "react-redux"
// This selector selects the user-permission from the redux-store
import { getUserPermissions } from "../selectors/permissions"
export const CheckPermissions = (props) => {
/**
* userPermissions: list of permissions the current user has
* route: the current route the user is in (route here is a
* react-router path)
* perms: the list of permissions, having any one of which
* should should render the component
*/
let { userPermissions, route, perms, children } = props
let [render, setRender] = useState(false)
// prevent any exceptions from crashing the app
// this is a crude approach and better approaches should be used
try {
!render && perms.forEach((perm) => {
if (userPermissions[route].includes(perm)) {
setRender(true)
}
})
if (true) {
return (
<div>
{children}
</div>
)
}
} catch {
return null
}
return null
}
// subscribe to the redux-store
const mapStateToProps = (state) => ({
userPermissions: getUserPermissions(state)
})
export default connect(mapStateToProps)(CheckPermissions)
/**
* Usage:
* <CheckPermissions route="some-route" perms=["list of permissions"]>
* <CheckedComponent />
* </CheckPermissions>
**/

The user-permissions were stored in the redux-store route-wise.

The above approach does the job but fails if the <CheckedComponent /> also requires permission checking, basically making the whole process nested. Also, I didn’t know before-hand the depth of nesting.

I could also have used HoCs, but they too suffered from the same issue, in my case.

So, I decided to go with the hooks approach, which looks something like this:

  • the useAuthorization hook returns the list of permissions the user has for the given route. This may seem repetitive as this could directly have been taken from the redux-store, but this hook provided a level of abstraction. Also, if the permission-retrieval logic for the user is changed, only this needed to be changed and everything would still work.
  • each component is conditionally rendered based on the user permissions.
    import { useSelector } from "react-redux"
    import { getUserPermissions } from "../selectors/permissions"
    // Hook to get current user's activity/authorization access for a route
    export const useAuthorization = (route) => {
    const userPermissions = useSelector(getUserPermissions)
    return userPermissions ? userPermissions[route] || [] : []
    }
    /**
    * The following lines show the usage and isn't a part of the hook.
    * CheckedComponent is the component which needs to be conditionally rendered.
    */
    const userAccess = useAuthorization("some-route")
    if(userAccess.includes("some-perm")) {
    return <CheckedComponent />
    } else return null
    In this approach, I need to do the same thing again and again, but looks neat for now.

Updating the conditions when permissions change:

In my case, whenever a user logged in, the server responded with a set of permissions associated with that user. I wrote a middleware which on login-success read the permissions and updated the redux-store. I am also using redux-persist to save authorization and authentication information about the user in the local storage.

Now, when the permissions are changed for any user, the user’s authorization-token is expired. This is very important or the user-permissions won’t be updated in the redux-store and no change will be visible. The changes with the new permissions are reflected when the user logins again.


This approach of RBAC may not be what’s ideal, but this works for me. A few important steps I have skipped over, as they are subjective to an application, viz.,

  • Agreeing on the structure of the permissions. In my case, I arranged the permissions route-wise , but you can use any other structure.
  • The redux-middleware part. This basically takes the server-response and transforms that into the structure mentioned in the first point.

This post is also availabe as a medium post and can be found here :

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (2)

Collapse
 
mosqueradvd profile image
𝘿𝘼𝙑𝙄𝘿 𝘼𝙇𝙀𝙅𝘼𝙉𝘿𝙍𝙊 𓅓

Amazing!

Do you have a repo for this project?

Collapse
 
tsuki42 profile image
Sudhanshu

Thank you!

Besides the code-snippets provided in the article, I don't really have a sample PoC project for this. Sorry.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay