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.
Please note that the following example implementation and the permit-fe-sdk
fully supports version 6 of CASL.
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
- yarn
npm install @casl/ability @casl/react permit-fe-sdk permitio
yarn add @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.
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>;
};
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
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>