📜 ⬆️ ⬇️

Approach to RBAC implementation in ReactJS

Introduction


Hello dear reader!

Some time (about a year) I was faced with the need for conditional rendering of components in ReactJS, depending on the current user rights. The first thing I began to look for ready-made solutions and "best practices." The article " Role based authorization in React " made the most of its impressions with its use of the Higher-Order Components (HOC). But, unfortunately, I did not find a solution that satisfies me.

Apparently, I missed something after all ...
... or did not know about the existence of contexts. At the time of this writing, I stumbled upon a wonderful answer in stackoverflow . I ended up with a very similar solution.

At that time, I was a little familiar with “react-redux-connect” (npm-module), and I was strongly hooked on the decoration approach used in the connect function. A detailed analysis of the connect device can be found here .

Solution Description


First you need to determine what minimum information is needed to make a decision about the component rendering. Obviously, for drawing it is necessary to fulfill some condition (as an option, the presence of some right - for example, the right to add a new user). Call this condition a requirement (or requirement in English). We can understand whether the requirement has been satisfied, based on the current set of user rights - credentials . That is, it is enough to define a function:
')
function isSatisfied(requirement, credentials) { if (...) { return false; } return true; } 

Now we have more or less decided on the rendering condition. How to use it?

1. We can use the approach in the forehead:

 const requirement = {...}; class App extends Component { render() { const {credentials} = this.props; return isSatisfied(requirement, credentials) && <TargetComponent>; } } 

2. We can go a little further, and wrap the target component in another, which will do the verification of the requirement:

 const requirement = {...}; class ProtectedTargetComponent extends Component { render() { const {credentials} = this.props; return ( isSatisfied(requirement, credentials) ? <TargetComponent {...this.props}> {this.props.children} </TargetComponent> : null ); } } class App extends Component { render() { const {credentials} = this.props; return <ProtectedTargetComponent/>; } } 

Manually writing a wrapper for each target component is rather a chore. How can this be simplified?

3. We can resort to the HOC mechanism (by analogy with the connect from “react-redux-connect”):

 function protect(requirement, WrappedComponent) { return class extends Component { render() { const { credentials } = this.props; return ( isSatisfied(requirement, credentials) ? <WrappedComponent {...this.props}> {this.props.children} </WrappedComponent> : null ); } } } ... const requireAdmin = {...}; const AdminButton = protect(requireAdmin, Button); ... class App extends Component { render() { const {credentials} = this.props; return ( ... <AdminButton credentials={credentials}> Add user </AdminButton> ... ); } } 

Already better, but still poor - you need to send credentials through the entire tree of components with your hands. What can be done with this? It is logical to assume that the current user's credentials is a global object for the entire application. Then “react-redux-connect” comes again. After reading the article about the device of this module, we find that it uses some ReactJS contexts .

4. Using the context mechanism, we get the final approach:

 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> ); } } } ... const requireAdmin = {...}; const AdminButton = protect(requireAdmin, Button); ... class App extends Component { render() { const { credentials } = this.props; return ( <Provider value={credentials}> ... <AdminButton> Add user </AdminButton> ... </Provider> ); } } 

Afterword


It was a brief insight into the idea itself. On the basis of this idea, a module was implemented ( github , npm ), which provides more interesting features and is easier to embed (see README.md in githubs and demos using the module).

Only for some reason I could not get the package created by npm in the demo, so I had to insert the module code itself there. But the module installed via npm install react-rbac-guard works locally (Chrome 69.0.3497.100). I suspect that the problem is in the build method - I just copied the package.json and webpack.config.prod.js files from a module with the same purpose.

Since I am not a front-end developer, there is still a lot of unfinished (lack of tests, inoperability in https://codesandbox.io and, possibly, other lost moments). Therefore, if there are comments, suggestions or pull-requests, then welcome!

PPS: All comments on spelling, including in README.md, please send in private messages or in the form of a pull-request.

Source: https://habr.com/ru/post/428143/


All Articles