Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hide some React component children depending on user role

I am writing a single page application in React and Redux (with a Node.js backend).

I want to implement role-based access control and want to control the display of certain parts (or sub parts) of the app.

I'm going to get permissions list from Node.js, which is just an object with such structure:

{
  users: 'read',
  models: 'write',
  ...
  dictionaries: 'none',
}

key is protected resource,

value is user permission for this resource (one of: none, read, write).

I'm storing it into redux state. Seems easy enough.

none permission will be checked by react-router routes onEnter/onChange hooks or redux-auth-wrapper. It seems easy too.

But what is the best way to apply read/write permissions to any component view (e.g. hide edit button in Models component if the user has { models: 'read' } permission).

I've found this solution and change it a bit for my task:

class Check extends React.Component {
  static propTypes = {
    resource: React.PropTypes.string.isRequired,
    permission: React.PropTypes.oneOf(['read', 'write']),
    userPermissions: React.PropTypes.object,
  };

  // Checks that user permission for resource is the same or greater than required
  allowed() {
    const permissions = ['read', 'write'];
    const { permission, userPermissions } = this.props;
    const userPermission = userPermissions[resource] || 'none';

    return permissions.indexOf(userPermission) >= permissions.indexOf(permission)
  }

  render() {
    if (this.allowed()) return { this.props.children };
  }
}

export default connect(userPermissionsSelector)(Check)

where userPermissionsSelector would be something like this: (store) => store.userPermisisons and returns user permission object.

Then wrap protected element with Check:

<Check resource="models" permission="write">
  <Button>Edit model</Button>
</Check>

so if user doesn't have write permission for models the button will not be displayed.

Has anyone done anything like this? Is there more "elegant" solution than this?

thanks!

P.S. Of course user permission will also be checked on the server side too.

like image 471
Anton Novik Avatar asked Mar 02 '17 22:03

Anton Novik


2 Answers

The approach that suggested by @lokuzt is great.

And you can go even further in order to simplify your code.

First of all, every protected component has some requirement to be satisfied for render. You need to define a function that takes requirement to render and credentials of the current user as parameters. It must return true or false.

function isSatisfied(requirement, credentials) {
  if (...) {
    return false;
  }

  return true;
}

Further, we have to define a HOC (Higher-Order Component) using the new context API from ReactJS.

const { Provider, Consumer } = React.createContext();

function protect(requirement, WrappedComponent) {
  return class extends Component {
    render() {
      return (
        <Consumer>
          { credentials => isSatisfied(requirement, credentials)
            ? <WrappedComponent {...this.props}>
                {this.props.children}
              </WrappedComponent>
            : null
          }
        </Consumer>
      );
    }
  }; 
}

Now you can decorate your components:

const requireAdmin = {...}; // <- this is your requirement

class AdminPanel extends Component {
  ...
}

export default protect(requireAdmin, AdminPanel);

or even third-party components:

import {Button} from 'react-bootstrap';

const AdminButton = protect(requireAdmin, Button);

Credentials have to be passed by ReactJS context API:

class MyApp extends Component {
  render() {
    const {credentials} = this.props;

    <Provider value={credentials}>
      ...

         <AdminPanel/>

         <AdminButton>
           Drop Database
         </AdminButton>
      ...
    </Provider>
  }
}

Here is my extended implementation on github.

The demo is also available too.

like image 159
n.saktaganov Avatar answered Nov 07 '22 04:11

n.saktaganov


Well I think I understood what you want. I have done something that works for me and I like the way I have it but I understand that other viable solutions are out there.

What I wrote was an HOC react-router style.

Basically I have my PermissionsProvider where I init the users permissions. I have another withPermissions HOC that injects the permissions I provided earlier into my component.

So if I ever need to check permissions in that specific component I can access them easily.

// PermissionsProvider.js
import React, { Component } from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";

class PermissionsProvider extends React.Component {
  static propTypes = {
    permissions: PropTypes.array.isRequired,
  };

  static contextTypes = {
    permissions: PropTypes.array,
  };

  static childContextTypes = {
    permissions: PropTypes.array.isRequired,
  };

  getChildContext() {
    // maybe you want to transform the permissions somehow
    // maybe run them through some helpers. situational stuff
    // otherwise just return the object with the props.permissions
    // const permissions = doSomething(this.props.permissions);
    // maybe add some validation methods
    return { permissions: this.props.permissions };
  }

  render() {
    return React.Children.only(this.props.children);
  }
}

const withPermissions = Component => {
  const C = (props, context) => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <Component permissions={context.permissions} {...remainingProps} ref={wrappedComponentRef} />
    );
  };

  C.displayName = `withPermissions(${Component.displayName || Component.name})`;
  C.WrappedComponent = Component;
  C.propTypes = {
    wrappedComponentRef: PropTypes.func
  };

  C.contextTypes = {
    permissions: PropTypes.array.isRequired
  };

  return hoistStatics(C, Component);
};

export { PermissionsProvider as default, withPermissions };

Ok I know this is a lot of code. But these are HOC (you can learn more here).

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature. Concretely, a higher-order component is a function that takes a component and returns a new component.

Basically I did this because I was inspired by what react-router did. Whenever you want to know some routing stuff you can just add the decorator @withRouter and they inject props into your component. So why not do the same thing?

  1. First you must setup the permissions with the provider
  2. Then you use the withPermissions decorator on the components that check permissions

//App render
return (
   <PermissionsProvider permissions={permissions}>
      <SomeStuff />
   </PermissionsProvider>
);

Somewhere inside SomeStuff you have a widely spread Toolbar that checks permissions?

@withPermissions
export default class Toolbar extends React.Component {
  render() {
    const { permissions } = this.props;

    return permissions.canDoStuff ? <RenderStuff /> : <HeCantDoStuff />; 
  }
}

If you can't use decorators you export the Toolbar like this

export default withPermissions(Toolbar);

Here is a codesandbox where I showed it in practice:

https://codesandbox.io/s/lxor8v3pkz

NOTES:

  • I really really simplified the permissions because that logic comes from your end and for demo purposes I simplified them.
  • I assumed the permissions were an array so that is why I check the PropTypes.array in the HOCs
  • It's a really long and complicated answer and I tried to articulate at my best ability. Please don't grill me for some mistakes here and there :)
like image 22
João Cunha Avatar answered Nov 07 '22 03:11

João Cunha