Eoin February 2016

setting permissions on redux actions

I am creating a web redux-react app which will have a number of different permission levels. Many users may be interacting with one one piece of data but some may have limitations on what they can do.

To me, the obvious way to set permissions on interactions on the data (held behind the app server) would be to associate certain permissions with different redux actions. Then, when a user saves their state the client side app would bundle up the users action history and send it back to the server. These actions could then be applied to the data in the server and permissions could be checked, action by action, against a user jwt.

This would mean lots our reducer code could be used isomorphically on the server.

I cannot find any resources/disscussions on this. What is the normal way of handling complex permissions in a redux app? Having auth purely at the endpoint seems cumbersome , this would require rewriting a ton of new code that is already written in client side reducers. Is the any reason not to go ahead and create a reducer which checks auth on each action?

Points:

  • We must assume actions sent to the server are authenticated, but sent by users that do not have permission dispatch these actions
  • If the permissions have been checked and are inside the actions then the reducer can check permissions and be pure

Answers


Florent February 2016

I think it's not the responsibility of action creators to check the permissions but using a reducer and a selector is definitively the way to go. Here is one possible implementation.

The following component requires some ACL checks:

/**
 * Display a user record.
 * 
 * A deletion link is added if the logged user has sufficient permissions to
 * delete the record.
 */
function UserRecord({ username, email, avatar, isGranted, deleteUser }) {
  return (
    <div>
      <img src={avatar} />
      <b>{username}</b>
      {isGranted("DELETE_USER")
        ? <button onClick={deleteUser}>{"Delete"}</button>
        : null
      }
    </div>
  )
}

We need to connect it to our store to properly hydrate all props:

export default connect(
  (state) => ({
    isGranted: (perm) => state.loggedUser.permissions.has(perm),
  }),
  {deleteUser},
  (stateProps, dispatchProps, ownProps) => ({
    ...stateProps,
    ...ownProps,
    deleteUser: () => dispatchProps.deleteUser(ownProps.user)
  })
)(UserRecord)
  • The first argument of connect will create isGranted for the logged user. This part could be done using reselect to improve performance.

  • The second argument will bind the actions. Nothing fancy here.

  • The third argument will merge all props and will pass them to the wrapped component. deleteUser is bound to the current record.

You can now use UserRecord without dealing with ACL checks since it will auto-update depending on what is stored in loggedUser.

<UserRecord user={someUser} />

In order to get the above example work you need to store the logged user in Redux's store as loggedUser. You don'


Grgur February 2016

You can set up an helper function that would be built into actions for checking user rights (locally or remotely) where you would also provide with a callback action creator on error. Of course redux-thunk or similar would be needed so you can dispatch actions from other actions.

The key rule you should observe here is:

Reducers are pure functions.

Action creators can be impure. That means reducers always return the same value given the same arguments. Checking for ACL rights in reducer will violate that rule.

Say let's say you need to fetch the list of contacts. Your action is REQUEST_CONTACTS. The action creator would first dispatch something like:

// ACL test function
function canAccessContacts(dispatch) {
    if (user !== 'cool') {
        dispatch({type: 'ACCESS_DENIED'});
        return false;
    }
}

// Action creator
function fetchContacts() {
    return (dispatch) => {
        if (!canAccessContacts(dispatch)) {
            return false;
        }

        // your logic for retrieving contacts goes here
        dispatch({
            type: 'RECEIVE_CONTACTS',
            data: your_contacts_data_here
        });
      };
}

RECEIVE_CONTACTS will be fired once you have data back. Time between REQUEST_CONTACTS and RECEIVE_CONTACTS (which is likely an async call) is ian opportunity to show your loading indicator.

Of course, this is a very raw example, but it should get you going.

Post Status

Asked in February 2016
Viewed 2,413 times
Voted 11
Answered 2 times

Search




Leave an answer