23

I have a React-Redux-KoaJs application with multiple components. I have few user roles as well. Now i want to display few buttons, tables and div to only specific roles and hide those from others. Please remember i dont want to hide the whole component, but just a part of the components. Can anyone help me? Thanks in advance.

Harshit Agarwal
  • 2,308
  • 2
  • 15
  • 23

6 Answers6

31

You can check the role or permission in every component as @Eudald Arranz proposed. Or you can write a component that will checks permissions for you. For example:

import PropTypes from 'prop-types';
import { connect } from 'react-redux';

const ShowForPermissionComponent = (props) => {
    const couldShow = props.userPermissions.includes(props.permission);
    return couldShow ? props.children : null;
};

ShowForPermissionComponent.propTypes = {
    permission: PropTypes.string.isRequired,
    userPermissions: PropTypes.array.isRequired
};


const mapStateToProps = state => ({
    userPermissions: state.user.permission //<--- here you will get permissions for your user from Redux store
});

export const ShowForPermission = connect(mapStateToProps)(ShowForPermissionComponent);

and then you can use this component like this:

import React from 'react';
import { ShowForPermission } from './ShowForPermission';

cons MyComponent = props => {
   return (
        <div>
            <ShowForPermission permission="DELETE">
                <button>Delete</button>
            </ShowForPermission>
        </div>
   );
}

Andrii Golubenko
  • 4,879
  • 1
  • 22
  • 27
  • Thanks for the answer. I really appreciate the answer. But i figured out an alternate approach, which i am going to put in the answers really soon. Do check it out. Your suggestions are welcome. – Harshit Agarwal May 07 '19 at 05:34
  • 4
    This approach has performance drawback. Imagine you have 150+ or more UI elements in your page that needs to checked for permissions (buttons, selects, checkboxes, menu options, tabs, etc etc). With your approach you end up calling `props.userPermissions.includes` method 150+ times or more. You're iterating the same array for every element. It may slowdown your app. – Green Feb 27 '20 at 09:15
  • 1
    @Green yes, you are right. You can save your permissions in ES6 `Set`, or if you don't like to use mutable collections, you could use `Set` from immutable.js. Or you could just use an `Object` where keys - permission names and values - whatever you want. – Andrii Golubenko Feb 27 '20 at 13:47
16

Be careful with that. If the actions of some roles are important you should always validate them at your backend. It's easy to change the values stored in redux at frontend allowing malicious use of the roles if there is no proper validation.

If you want to proceed on a possible approach is this:

  • Save the roles at your reducer
  • Bind the reducer to the component:
function mapStateToProps(state) {
  const { user_roles } = state;
  return { user_roles };
}

export default connect(mapStateToProps)(YourComponent);
  • Then at your component, you can check the user_roles and render the actions accordingly:
render() {
    return (
      <div>
        {this.props.user_roles.role === "YOUR_ROLE_TO_CHECK" && <ActionsComponent />}
      </div>
    );
  }

This will render the ActionsComponent only when the role is equal to the desired one.

Again, always validate the roles at your backend!

  • 2
    Thanks for the answer. This is one approach to follow but if you have a lot of components and elements to show and hide then it becomes really hard to keep track of it. I have figured out an alternate approach which i am going to post in answers real soon. Do check it out. Your suggestions are welcome. – Harshit Agarwal May 07 '19 at 05:36
  • 3
    @Harshit it has been 3 years now. Where is your alternate approach that you were providing _real soon_ – Simple Fellow Jul 29 '21 at 04:26
12

The best practice to solve this Problem is, simply prevent the app to generate unnecessary routes, rather checking current user role on each route it is great to generate only the routes that user have access.

So The Normal reouting is: To control the whole view:

const App = () => (
  <BrowserRouter history={history}>
    <Switch>
      <Route path="/Account" component={PrivateAccount} />
      <Route path="/Home" component={Home} />
    </Switch>
  </BrowserRouter>
  export default App;
);

Routing based on user role:

import { connect } from 'react-redux'
   // other imports ...
   const App = () => (
      <BrowserRouter history={history}>
        <Switch>
        {
          this.props.currentUser.role === 'admin' ?
            <>
          <Route path="/Account" exact component={PrivateAccount} />
          <Route path="/Home" exact component={Home} />
            </> 
            : 
          <Route path="/Home" exact component={Home} />
        }
        <Route component={fourOFourErroPage} />

        </Switch>
      </BrowserRouter>
      
const mapStateToProps = (state) => {
  return {
    currentUser: state.currentUser,
  }
}
export default connect(mapStateToProps)(App);

So the user with an Admin role will have access to the Account page and for other users will have access to the Home page Only! and if any user try to access to another route, the 404 page error will appear. I hope I've given a helpful solution.

For advanced details about this approach you can check this repo on github: Role-based-access-control with react

To hide just a presentational component:

{this.props.currentUser.role === 'admin' && <DeleteUser id={this.props.userId} /> }
Nbenz
  • 602
  • 1
  • 9
  • 18
6

So, I have figured out there is an alternate and easy approach to implement role based access (RBAC) on frontend.

In your redux store state, create a object called permissions (or you can name it whatever you like) like this:

const InitialState = {
  permissions: {}
};

Then on your login action, setup the permissions that you want to provide like this:

InitialState['permissions'] ={
  canViewProfile: (role!=='visitor'),
  canDeleteUser: (role === 'coordinator' || role === 'admin')
  // Add more permissions as you like
}

In the first permission you are saying that you can view profile if you are not a visitor. In the second permission you are saying that you can delete a user only if you are an admin or a coordinator. and these variables will hold either true or false on the basis of the role of the logged in user. So in your store state u will have a permission object with keys that represent permissions and their value will be decided on the basis of what your role is.

Then in your component use the store state to get the permission object. You can do this using connect like:

const mapStateToProps = (state) => {
  permissions : state.permissions
}

and then connect these props to your Component like:

export default connect(mapStateToProps,null)(ComponentName);

Then you can use these props inside your component on any particular element which you want to show conditionally like this:

{(this.props.permissions.canDeleteUser) && <button onClick={this.deleteUser}>Delete User</button>}

The above code will make sure that the delete user button is rendered only if you have permissions to delete user i.e. in your store state permissions object, the value of canDeleteUser is true.

That's it, you have appplied a role based access. You can use this approach as it is easily scalable and mutable, since you will have all permsisions according to roles at one place.

Hope, this helps! If i missed out something please help me in the comments. :-)

Harshit Agarwal
  • 2,308
  • 2
  • 15
  • 23
  • 5
    > _it is easily scalable and mutable_ It is not easily scalable. It is good only for simple use cases like yours - a couple of roles and a couple of permissions. Anything more complicated than that (e.g different stages of the app with different permissions for roles on each stage) and you will end up having this: `a && b || c && d || e` which is hard to manage. – Green Feb 27 '20 at 09:01
  • @Green so what is the best way to manage scalable user roles and permissions in SPAs and possibly sync with the backend too? – Yehya Dec 15 '20 at 07:16
1

I have implemented this in this rbac-react-redux-aspnetcore repository. If someone wants to use Redux with Context API, then the below code snippet can be helpful.

export const SecuedLink = ({ resource, text, url }) => {

  const userContext = useSelector(state => {
    return state.userContext;
  });    

  const isAllowed = checkPermission(resource, userContext);
  const isDisabled = checkIsDisabled(resource, userContext);

  return (isAllowed && <Link className={isDisabled ? "disable-control" : ""} to={() => url}>{text}</Link>)
}


const getElement = (resource, userContext) => {
    return userContext.resources
        && userContext.resources.length > 0
        && userContext.resources.find(element => element.name === resource);
}

export const checkPermission = (resource, userContext) => {
    const element = getElement(resource, userContext);
    return userContext.isAuthenticated && element != null && element.isAllowed;
}

export const checkIsDisabled = (resource, userContext) => {
    const element = getElement(resource, userContext);
    return userContext.isAuthenticated && element != null && element.isDisabled;
}

To use the above snippet, we can use it like below

  <SecuedLink resource='link-post-edit' url={`/post-edit/${post.id}`} text='Edit'></SecuedLink>
  <SecuedLink resource='link-post-delete' url={`/post-delete/${post.id}`} text='Delete'></SecuedLink>

So, depending on the role, you can not only show/hide the element, but also can enable/disable them as well. The permission management is fully decoupled from the react-client and managed in database so that you don't have to deploy the code again and again just to support new roles and new permissions.

Foyzul Karim
  • 4,252
  • 5
  • 47
  • 70
0

This post is very exactly what you need without any library:

react-permissions-and-roles

Abdelhadi Abdo
  • 392
  • 2
  • 9