Control frontend features with CASL and Permit
Show or hide parts of your React or Next.js UI based on the permissions of the signed-in user, with the permit-fe-sdk package and CASL. This guide is for frontend developers who already have a Permit.io policy and want the UI to match what the backend allows.
CASL is an open-source JavaScript library that describes what a user can do as a list of rules, called an ability. The permit-fe-sdk package loads permission results for the signed-in user from your backend, which runs permit.check() against your policy decision point (PDP), and turns the results into CASL rules.
A user can change code that runs in the browser. Use frontend checks to decide what the UI shows, and enforce every permission again in your backend with permit.check().
How the pieces fit together
- When a user signs in, the
AbilityLoadercomponent callsloadLocalStateBulk()frompermit-fe-sdkwith a list of actions and resources. loadLocalStateBulk()sends onePOSTrequest to your backend route, at<backendUrl>?user=<loggedInUser>, with the list in aresourcesAndActionsbody field.- The backend route runs
permit.check()for each item against the PDP, and returns the results as apermittedListarray of booleans, in the same order. permit-fe-sdkstores the results inpermitState, andpermitState.getCaslJson()returns them as CASL rules.- Your components read the results with
permitState.check()and render only what the user is allowed to see.
Prerequisites
- A Permit.io policy with the resources and actions you check. See Configure your first RBAC policy.
- Your users synced to Permit with the same user key your authentication provider returns. See Sync users.
- Your environment API key. See Get your API key.
- A running PDP. The example backend route connects to a PDP container at
http://localhost:7766. See Run the PDP. - A React or Next.js app with an authentication provider. The example uses Clerk. Any authentication provider works if it gives you the ID of the signed-in user.
Add CASL and Permit to a React or Next.js app
1. Install the packages
Install CASL, the CASL React bindings, permit-fe-sdk for the frontend, and permitio (the Permit Node.js SDK) for the backend route:
- npm
- yarn
npm install @casl/ability @casl/react permit-fe-sdk permitio
yarn add @casl/ability @casl/react permit-fe-sdk permitio
2. Create a backend route for bulk permission checks
permit-fe-sdk doesn't call the PDP directly, because the call needs your API key. Instead, it calls a route in your backend. The route receives the user key in the user query parameter and the list of checks in the resourcesAndActions body field, runs permit.check() for each item, and returns all results in one response. The frontend loads every result before the UI renders.
This example is a Next.js API route. You can give the file any name. The frontend code later in this guide calls the route at /api/something:
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" });
}
}
In the route:
- Replace
YOUR_PERMIT_API_KEYwith your environment API key. In a real app, load the key from an environment variable. Anyone with the key can change the environment's policy through the Permit API. pdpis the address of your PDP. Change it if your PDP doesn't run onlocalhost:7766.userAttributesandresourceAttributesare optional. Pass them for attribute-based access control (ABAC) policies. Role-based access control (RBAC) checks don't need them.- Every check uses the
defaulttenant. Changetenantif your users belong to other tenants.
3. Create the AbilityLoader component
The AbilityLoader component loads the permissions of the signed-in user once, after sign-in, and shares them with the rest of the app. This example gets the user ID from Clerk:
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>;
};
Replace the resource keys and actions in the loadLocalStateBulk() list with the resources and actions of your policy. Set backendUrl to the path of your backend route. loadLocalStateBulk() runs its request only once per page load. Later calls return without sending another request.
The next sections explain each part of the AbilityLoader component.
Share the abilities with a React context
AbilityContext is a React context that holds the CASL ability of the signed-in user. Any component that reads the context gets the ability, without passing it through props at each level:
// Create Context
export const AbilityContext = createContext();
Load the permission results
The getAbility function takes the ID of the signed-in user and:
- Creates a
permit-fe-sdkclient withPermit().loggedInUseris the user key that Permit checks, andbackendUrlis your backend route. - Calls
loadLocalStateBulk()with the actions and resources to check. The function sends the list to the backend route and stores the results inpermitState. - Converts the results to CASL rules with
permitState.getCaslJson(), and creates a CASLAbilityfrom the rules. Each rule has anaction, asubject(the resource), andinverted: truewhen the check denied the action.
This excerpt shows the function from the component:
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;
};
Provide the context to child components
The AbilityLoader component returns the context provider with the loaded ability, so the component can wrap the whole app:
<AbilityContext.Provider value={ability}>{children}</AbilityContext.Provider>
4. Wrap your app with AbilityLoader
Wrap your app in <AbilityLoader> so that permissions load when a user signs in. In a Next.js app with the Pages Router, wrap the page component in pages/_app.js. AbilityLoader uses Clerk's useUser(), so it goes inside ClerkProvider:
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;
5. Render components based on permissions
Import permitState in each file that renders UI based on a permission:
import { permitState } from "permit-fe-sdk";
permitState.check() returns the stored result for an action and resource. It doesn't call the backend. If the action and resource weren't in the loadLocalStateBulk() list, permitState.check() returns false.
Render based on an RBAC permission
Pass the action and the resource key. This element renders only when the user can view a document:
<div>
{permitState?.check("view", "document") && (
<div className="bg-white m-4 p-4 h-full">Document</div>
)}
</div>
Render based on an ABAC permission
For an ABAC check, pass the same resource key and resource attributes that you passed to loadLocalStateBulk(). The permit-fe-sdk source defines permitState.check(action, resource, resourceAttributes): the third argument is the resource attributes. User attributes come from the userAttributes option of Permit():
<div>
{permitState?.check("view", "files_for_poland_employees", { country: "PL" }) && (
<div className="bg-white m-4 p-4 h-full">Files for Poland employees</div>
)}
</div>
Check ABAC permissions with attributes
The Cloud PDP supports RBAC and relationship-based access control (ReBAC) only. If your backend route sends checks to the Cloud PDP, ABAC policies that use user or resource attributes don't evaluate. Use an Edge PDP for ABAC. See Cloud PDP capabilities.
The same AbilityLoader handles RBAC and ABAC checks. For an ABAC check, add userAttributes and resourceAttributes to an item in the loadLocalStateBulk() list. The backend route passes the attributes to permit.check(), and the PDP evaluates the conditions of your ABAC policy:
await permit.loadLocalStateBulk([
{ action: 'view', resource: 'statement' },
{ 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' },
},
]);
In this list, the last item asks whether a user in the Engineering department can view resources of the files_for_poland_employees type with country set to PL.
Verify the integration
- Sign in to your app as a user who has a role that allows
viewondocument. - In the browser developer tools, open the Network tab. Confirm one
POSTrequest to your backend route with theuserquery parameter, and a response with apermittedListarray that has one boolean per item in your list. - Confirm that the
Documentelement renders. - Sign in as a user without that permission. The
permittedListentry fordocumentisfalse, and theDocumentelement doesn't render.
If every result is false, check the backend route's console output for the results, check that the user key matches a user synced to Permit, and check the decisions in the audit log.