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.
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?
//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: