Skip to main content
Version: 2.0.0

CASL

CASL is a versatile, open-source JavaScript library widely used for managing Access Control Lists (ACLs) and implementing Role-Based Access Control (RBAC) in JavaScript applications. It allows developers to enforce granular permissions within applications, ensuring that resources and actions are accessible only to authorized users or roles across various technology stacks.

Permit.io & CASL

Leveraging CASL and Permit.io together, developers can create a refined system where frontend feature flags are controlled based on user permissions and roles. This synergy not only facilitates a secure and tailored user experience but also empowers development teams to manage and roll out new features effectively and safely across diverse user bases.

Our frontend SDK

In our dedicated pursuit to enhance developer experience and streamline frontend feature management, we crafted a specialized package at Permit.io, encapsulating both Permit and CASL. The permit-fe-sdk enables developers to integrate sophisticated permission-based feature toggling seamlessly across various frontend frameworks.

React/NextJS Implementation

In order to start using the permit-fe-sdk, below is a step-by-step guide on how to incorporate it into your React or NextJS app.

Installing the relevant packages

npm install @casl/ability @casl/react permit-fe-sdk permitio

Create an API route to handle the permissions checks

As part of the CASL component that we will be creating later in this guide, we will need to specify an API route that we can call, which will perform bulk permit.check() operations for us, returning the result for each.

tip

The bulk permit.check() allows us to send over multiple permit check queries to run against the policy engine at once. This allows us to get all the answers, before we start to render the app and it's UI. q1 1§1

Here is a basic implementation of such endpoint, which we have under /api/something. You can name the file as you wish.

import { Permit } from "permitio";

const permit = new Permit({
token: "YOUR_PERMIT_API_KEY",
pdp: "http://localhost:7766",
});

export default async function handler(req, res) {
try {
const { resourcesAndActions } = req.body;
const { user: userId } = req.query;

if (!userId) {
return res.status(400).json({ error: "No userId provided." });
}

const checkPermissions = async (resourceAndAction) => {
const { resource, action, userAttributes, resourceAttributes } = resourceAndAction;

const allowed = permit.check(
{
key: userId,
attributes: userAttributes,
},
action,
{
type: resource,
attributes: resourceAttributes,
tenant: "default",
}
);

return allowed;
};

const permittedList = await Promise.all(resourcesAndActions.map(checkPermissions));

console.log(permittedList); // Printing the result of the checks

return res.status(200).json({ permittedList });
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error" });
}
}

Pay attention that we have imported the Permit library and we initialized the Permit object. You can follow this guide to understand where to fetch your API key from.

This handler also allows you to deal with two policy models: RBAC & ABAC. As part of the bulk check, you can pass userAttributes and resourceAttributes - but they are optional.

Once we are done with this step, we need to make sure pull and launch our PDP - you can also find the steps on how to do this here.

Creating the AbilityLoader Component

The AbilityLoader component is integral to this setup, diligently working to asynchronously retrieve and establish user-specific permissions, particularly upon user sign-in.

In this scenario, we're employing Clerk.com as our authentication provider to obtain the userId, which we have synchronized with Permit. This allows us to identify the currently logged-in user and correlate them with the associated policy for their role. It's crucial to highlight that you can choose any authentication provider that best fits your needs—Permit is designed to integrate seamlessly with all of them.

import React, { createContext, useEffect, useState } from "react";
import { useUser } from "@clerk/nextjs";
import { Ability } from "@casl/ability";
import { Permit, permitState } from "permit-fe-sdk";

// Create Context
export const AbilityContext = createContext();

export const AbilityLoader = ({ children }) => {
const { isSignedIn, user } = useUser();
const [ability, setAbility] = useState(undefined);

useEffect(() => {
const getAbility = async (loggedInUser) => {
const permit = Permit({
loggedInUser: loggedInUser,
backendUrl: "/api/something",
});

await permit.loadLocalStateBulk([
{ action: "view", resource: "Products" },
{ action: "view", resource: "document" },
{ action: "view", resource: "file" },
{ action: "view", resource: "component" },
]);

const caslConfig = permitState.getCaslJson();

return caslConfig && caslConfig.length ? new Ability(caslConfig) : undefined;
};

if (isSignedIn) {
getAbility(user.id).then((caslAbility) => {
setAbility(caslAbility);
});
}
}, [isSignedIn, user]);

return <AbilityContext.Provider value={ability}>{children}</AbilityContext.Provider>;
};
note

In this guide, we do not cover how you should sync your user into Permit, nor how to setup your policy.

Let's break the above code down into smaller chunks so we know exactly what is happening.

Creating a Context

The AbilityContext is crafted to share the user's permissions (or "abilities"), which are managed and determined by CASL, across different components.

When AbilityContext is employed, it allows the permissions (or "abilities") data to be accessible and utilized by any component that consumes this context, without the need to prop-drill the data through numerous component layers.

// Create Context
export const AbilityContext = createContext();

Getting abilities

This step uses the id of the presently logged-in user from the session, transmitting a request to the previously defined API endpoint, and subsequently loading each of the actions and resources into the local state.

It executes permit checks against the established policy to ascertain whether the current user is authorized to execute the specified actions on the defined resources. Subsequently, the outcome is stored in permitState, from which the results of the permission checks are returned.

const getAbility = async (loggedInUser) => {
const permit = Permit({
loggedInUser: loggedInUser,
backendUrl: "/api/dashboard",
});

await permit.loadLocalStateBulk([
{ action: "view", resource: "Products" },
{ action: "view", resource: "Product_Configurators" },
{ action: "view", resource: "Project_Builder" },
{ action: "view", resource: "Topics_for_you" },
]);

const caslConfig = permitState.getCaslJson();

return caslConfig && caslConfig.length ? new Ability(caslConfig) : undefined;
};

Working with RBAC & ABAC

be careful

If your are using the cloud PDP for running your permit checks, please be aware that the cloud PDP only supports RBAC for the time being. ABAC policies (adding extra user and resource attributes) will fail.

The AbilityLoader supports both basic RBAC policies and ABAC policies, where you can check a users permissions not just based on their role, but also extra conditions on the role, or the resource.

You can simply just pass them into the loadLocalStateBulk function as userAttributes and resourceAttributes. The conditions will be evaluated in the same permit.check function and the result will be returned.

await permit.loadLocalStateBulk([
{ action: 'view', resource: 'statament' },
{ action: 'view', resource: 'products' },
{ action: 'delete', resource: 'file' },
{ action: 'create', resource: 'document' },
{
action: 'view',
resource: 'files_for_poland_employees',
userAttributes: {
department: "Engineering",
salary: "100K"
}
resourceAttributes: { country: 'PL' },
},
]);

Returning the AbilityContext.Provider

The last step in the AbilityLoader is to enable this component to be a wrapper for the whole app, so that the abilities are globally accessible.

<AbilityContext.Provider value={ability}>{children}</AbilityContext.Provider>

Wrapping our App with the AbilityLoader

Once we are ready to utilize the abilities to render parts of the UI based on the users permissions, we need to wrap our whole application in the <AbilityLoader> component, getting access to the permitState globally.

import { ClerkProvider } from "@clerk/nextjs";
import { AbilityLoader } from "../utils/AbilityLoader";
import "../styles/global.css";

function MyApp({ Component, pageProps }) {
return (
<ClerkProvider {...pageProps}>
<AbilityLoader>
<Component {...pageProps} />
</AbilityLoader>
</ClerkProvider>
);
}

export default MyApp;

Conditionally rendering components with permitState

First of all, in the file where you want to render part of the UI based on a condition, make sure you import permitState.

import { permitState } from "permit-fe-sdk";

Then, utilize permitState to render parts of the HTML.

Conditional render for RBAC

<div>
{permitState?.check("view", "document") && (
<div className="bg-white m-4 p-4 h-full">Document</div>
)}
</div>

Conditional render for ABAC

<div>
{permitState?.check(
"view",
"files_for_poland_engineers",
{
department: "Engineering",
salary: "100K",
},
{
country: "PL",
}
)}
</div>