DEV Community

Cover image for React, Manage Dynamic Permissions Using CASL & Redux.
Youssef Zidan
Youssef Zidan

Posted on • Edited on

React, Manage Dynamic Permissions Using CASL & Redux.

What is CASL?

CASL is a JavaScript library that you can manage the permissions of a user based on his role.

In this article, I will show you how to manage permissions with CASL in the Front-End using React and Redux.

Why handle permissions in the Front-End?

One of our roles as Front-End developers is to reduce the number of requests sending to the server.

For example, we do Front-End validations of a form so we don't have to request the server with the data, and the server reply to us with validation errors.

We will also manage permissions in the front end. so the user doesn't have to request certain APIs that he/she doesn't have permission for them. Eventually, we will reduce the load on the server and for the user.

That doesn't mean we will eliminate requesting unauthorized permissions APIs totally, We will need some of them in particular cases.

1. Getting Started.

You can download the project repo from HERE

You can find the final result HERE

  1. Create a react app.
npx create-react-app casl-app
Enter fullscreen mode Exit fullscreen mode
  1. install Redux, react-redux, and redux-thunk
npm install redux react-redux redux-thunk
Enter fullscreen mode Exit fullscreen mode
  1. install CASL
npm install @casl/react @casl/ability
Enter fullscreen mode Exit fullscreen mode

2. Creating Can File.

Create a new file and name it can.js and paste the following.

can.js

import { Ability, AbilityBuilder } from "@casl/ability";

const ability = new Ability();

export default (action, subject) => {
  return ability.can(action, subject);
};
Enter fullscreen mode Exit fullscreen mode

Here we are importing Ability and AbilityBuilder from @casl/ability.

Then we are creating a new instance from the Ability().

After that, we are exporting a default function that we will use later to check for the permission of the logged-in user.

3. Subscribing to the store.

can.js

import { Ability, AbilityBuilder } from "@casl/ability";
import { store } from "../redux/storeConfig/store";

const ability = new Ability();

export default (action, subject) => {
  return ability.can(action, subject);
};

store.subscribe(() => {
  let auth = store.getState().auth;
});

Enter fullscreen mode Exit fullscreen mode

Import your store and subscribe to it inside can.js.

Here I'm getting auth from the store.
And this is my redux folder and files:

store.js

import { createStore, applyMiddleware, compose } from "redux";
import createDebounce from "redux-debounced";
import thunk from "redux-thunk";
import rootReducer from "../rootReducer";

const middlewares = [thunk, createDebounce()];

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  rootReducer,
  {},
  composeEnhancers(applyMiddleware(...middlewares))
);

export { store };

Enter fullscreen mode Exit fullscreen mode

rootReducer.js

import { combineReducers } from "redux";
import authReducer from "./auth/authReducer";

const rootReducer = combineReducers({
  auth: authReducer,
});

export default rootReducer;

Enter fullscreen mode Exit fullscreen mode

authReducer.js

const INITIAL_STATE = {};

const authReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case "LOGIN":
      return { ...state, ...action.payload };
    case "LOGOUT":
      return {};
    default:
      return state;
  }
};

export default authReducer;

Enter fullscreen mode Exit fullscreen mode

authActions.js

export const login = (user) => async (dispatch) => {
  dispatch({
    type: "LOGIN",
    payload: {
      id: 1,
      name: "Youssef",
      permissions: ["add_users", "delete_users"],
    },
  });
};

export const logout = () => async (dispatch) => {
  dispatch({
    type: "LOGOUT",
  });
};

Enter fullscreen mode Exit fullscreen mode

In the login action, I'm hard coding the payload with an object of id, name, and permissions array.

permissions array doesn't have to be like that more about that later.

4. Add defineRulesFor function in can.js

import { Ability, AbilityBuilder } from "@casl/ability";
import { store } from "../redux/storeConfig/store";

const ability = new Ability();

export default (action, subject) => {
  return ability.can(action, subject);
};

store.subscribe(() => {
  let auth = store.getState().auth;
  ability.update(defineRulesFor(auth));
});

const defineRulesFor = (auth) => {
  const permissions = auth.permissions;
  const { can, rules } = new AbilityBuilder();

  // This logic depends on how the
  // server sends you the permissions array
  if (permissions) {
    permissions.forEach((p) => {
      let per = p.split("_");
      can(per[0], per[1]);
    });
  }

  return rules;
};
Enter fullscreen mode Exit fullscreen mode

I Created defineRulesFor function that takes auth as an argument and we will get this auth from the store we are subscribing to it.
so, I added ability.update(defineRulesFor(auth)) to the store.subscribe() body.

Then I'm getting can and rules from new AbilityBuilder()

And because my permissions array is a number of strings separated by _

permissions: ["add_users", "delete_users"]
Enter fullscreen mode Exit fullscreen mode

I'm splitting those strings and passing the action and the subject to the can function.

This logic might change if the server is sending you just Ids to be something like that:

const permissions = [2, 3, 5, 7];
if (permissions) {
  permissions.forEach((p) => {
    if (p === 3) can("add", "users");
    if (p === 7) can("delete", "users");
  });
}

Enter fullscreen mode Exit fullscreen mode

Or maybe a pre-defined role.

const role = "Editor";
if (role === "Editor") {
  can("add", "users");
  can("delete", "users");
}

Enter fullscreen mode Exit fullscreen mode

And so on.

5. Checking Permissions.

We will check permissions inside App.jsx.

App.jsx

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { login, logout } from "./redux/auth/authActions";
import CAN from "./casl/can";

export default () => {
  const dispatch = useDispatch();
  const { auth } = useSelector((state) => state);

  // rerender the component when `auth` changes
  useState(() => {}, [auth]);

  return (
    <React.Fragment>
      <h1>Welcome, {auth?.name || "Please Login!"}</h1>

      {CAN("add", "users") && (
        <button
          onClick={() => {
            alert("User Added!");
          }}>
          Add User
        </button>
      )}
      {CAN("delete", "users") && (
        <button
          onClick={() => {
            alert("User Deleted!");
          }}>
          Delete User
        </button>
      )}
      <div>
        <button
          onClick={() => {
            dispatch(login());
          }}>
          Login
        </button>
        <button
          onClick={() => {
            dispatch(logout());
          }}>
          Logout
        </button>
      </div>
    </React.Fragment>
  );
};

Enter fullscreen mode Exit fullscreen mode

Here I'm displaying the buttons based on the permission of the logged-in user.

You need to rerender the component when the auth changes using useEffect.

Check the final result HERE


LinkedIn

Top comments (2)

Collapse
 
ndunks profile image
Mochamad Arifin

What the mean of "Dynamic" here? Does it can assign/delete permissions without modifying code?

Collapse
 
youssefzidan profile image
Youssef Zidan

dynamic here means a dynamic permissions coming from an http request for example