Generate Personalized Frontend Experiences with User Attributes and Feature Flags
- Share:
Creating custom user experiences is a must for basically any application. This experience could include limiting the use of new features in your app to a select group of users or customizing the app experience of paying users based on their payment status or location.
Building these experiences yourself from scratch can be quite a challenge - they have to be secure, scalable, event-driven, and easy to manage.
In this blog, I’ll show you how to create a demo application that dynamically displays UI components (tiles) based on user roles and permissions using Attribute-Based Access Control (ABAC) and Feature Flagging. This approach personalizes the app experience and ensures that users only see content they are authorized to access.
The application will consider various resources and their attributes, allowing it to enforce granular permissions and provide the needed UX features.
To achieve that, we will be using Permit.io - an authorization-as-a-service solution that allows you to create dynamic, personalized app experiences without having to write your own complex authorization system from scratch.
The Challenge of Building Custom User Experiences
Creating a custom user experience that limits user access to parts of your app is not easy, as there are a lot of factors to consider here:
Security: Above all else, a solution that limits user access inside an application must be secure - ensuring that sensitive user data and permissions are protected. Unauthorized access must be prevented at all costs.
Scalability: This feature needs to keep up with, hopefully, a constantly increasing number of users in our app without compromising performance.
Event-driven Architecture: A system that uses attributes like payment status must obviously be able to react in real-time to changes in user attributes or status. Granting or revoking access must take effect immediately, both for security and UX’s sake.
Ease of Management: Managing this feature and the permissions we grant should be straightforward, allowing for quick updates and minimal overhead.
Flexibility: The system should adapt to various use cases and integrate seamlessly with existing your existing app infrastructure and services.
To achieve all of these requirements, we will be using Permit. Permit helps overcome these challenges by decoupling policy and code, integrating live data and policy updates, and a no-code UI for managing authorization policies.
What Will We Be Building?
In this guide, we'll create a demo application that dynamically displays tiles based on a user's role and permissions. Our app will leverage Attribute-Based Access Control (ABAC) and Feature Flagging to enforce granular permissions and tailor the user experience.
ABAC is a flexible security model that leverages user and resource attributes for access control decisions, offering more granularity compared to traditional Role-Based Access Control (RBAC).
A significant advantage of ABAC is its ability to utilize attributes directly from the user's Identity Provider (IDP), ensuring access decisions are always based on the most current data. Using the Permit dashboard will allow us to dynamically load tiles relevant to specific factors, such as the user's country and associated sales channel, personalizing and securing each user experience in line with their access rights and organizational role.
Prerequisites
Before we begin, make sure you have the following prerequisites in place:
- A Permit.io account
- A properly configured PDP (Policy Decision Point) with the correct API Key
- The Permit SDK package installed
- An authentication provider of your choosing (Permit works with any authentication provider). For this tutorial, we used clerk.com
- A basic app where you'll enforce permissions and adjust the UI. For this demo, you can clone a
nextjs/react
boilerplate starter.
Modeling basic roles and resources
As part of this project, we will be working with the fe-permit-sdk
and CASL to build a feature-toggling solution that uses Permit to manage who can view specific components in your application.
As mentioned previously, we’ll use Attribute-based Access Control to decide which components are rendered. This method allows us to check not only against a role but also against specific conditions. If you need a deeper dive into ABAC and modeling it in Permit, check out this guide.
The first step is to define the attributes we will be working with. Clerk, the authentication provider we’ll use in this guide, allows us to store metadata for individual users. In this case, we will store each user's country
and channel
.
The attributes defined in the authentication provider need to be mapped into the Permit. Permit requires knowing the user's name and data type. In this case, both are String
s, but many different data types are available.
To begin, we will need to define each UI component that we want to render conditionally as a resource in Permit. Each of these resources will have relevant conditional attributes.
To Define Resources in Permit:
- Log into your Permit.io account
- In case you haven’t already, Go to the Policy section. In the ABAC Rules tab, enable ABAC options.
- Go to the Policy Section
- Click the Resources tab
- Click Add Resource, and fill in the details of the UI component you want to render conditionally
- In the ABAC Options section, add the relevant attributes.
7. Each resource will need corresponding attributes defined as part of it. We will use the values of these resources for our loadLocalStateBulk
function, passing in the resource
, resource attributes
, and action
.
To control access to resources within an application, we must first define a Viewer
role that outlines specific permissions for accessing resources. This role allows us to set up a policy to define which resources are viewable by which users.
To Define Roles in Permit:
- Go to the Policy section
- Navigate to the Roles tab
- Click Add Role
- Create a new
Viewer
role - Click save
We want resources to be conditionally rendered based on the following policy:
if a user has the "view" permission for a particular resource, that resource will be displayed to them
This approach ensures that each user sees only the resources they are authorized to access.
Let’s recreate this policy via Permit’s no-code UI.
To Create an Access Control Policy in Permit:
- Go to the Policy section
- Navigate to the Policy Editor tab
- Check the relevant policies you wish your role to possess.
- Click Save Changes
Here is an example where we can only view one resource called Topics for you
We can also adjust the policy to view all the components:
Rendering the UI based on permissions
Next, we want CASL to render our components based on this example project. If you want to learn more about CASL and how it works, check out our in-depth guide here.
With the Permit side of things set up, we will need to create an API endpoint to handle permission checks.
Creating an API endpoint to handle permission checks
As part of the CASL component that we will be creating further in this guide, we will need to specify an API route that we can call. This API route will perform bulk permit.check()
operations for us, returning the result for each.
Here is a basic implementation of such an 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>",
});
exportdefaultasyncfunction 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" });
}
}
Notice that we have imported the Permit library, and initialized the Permit object.
To better understand where to fetch your API key, follow this guide.
Once we are done with this step, we need to pull and launch our PDP—the steps to do this are also available here.
Creating the AbilityLoader
The AbilityLoader component is integral to this setup, working to asynchronously retrieve and establish user-specific permissions, particularly upon user sign-in.
In this scenario, we're employing Clerk 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.
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
exportconst AbilityContext = createContext();
exportconst 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",
userAttributes: {
country: "PL",
channel: "ABC",
},
},
{
action: "view",
resource: "Product_Configurators",
userAttributes: {
country: "UK",
channel: "ABC",
},
},
{
action: "view",
resource: "Project_Builder",
userAttributes: {
country: "FR",
channel: "DEF",
},
},
{
action: "view",
resource: "Topics_for_you",
userAttributes: {
country: "ES",
channel: "DEF",
},
},
]);
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>;
};
Conditionally rendering the UI
First, make sure you import permitState
in the file where you want to render part of the UI based on a condition.
import { permitState }from "permit-fe-sdk";
Then, utilize permitState to render parts of the HTML.
<div className="flex h-full">
<div className="flex flex-col flex-grow">
{permitState?.check("view", "Products", {
country: user.publicMetadata.country,
channel: user.publicMetadata.channel,
}) && <div className="bg-white m-4 p-4 h-[250px]">Products</div>}
{permitState?.check("view", "Product_Configurators", {
country: user.publicMetadata.country,
channel: user.publicMetadata.channel,
}) && <div className="bg-white m-4 p-4 h-[200px]">Product Configurators</div>}
{permitState?.check("view", "Project_Builder", {
country: user.publicMetadata.country,
channel: user.publicMetadata.channel,
}) && <div className="bg-white m-4 p-4 h-[200px]">Project Builder</div>}
{permitState?.check("view", "Topics_for_you", {
country: user.publicMetadata.country,
channel: user.publicMetadata.channel,
}) && <div className="bg-white m-4 p-4 h-[100px]">Topicsfor you</div>}
</div>
</div>
Let’s See What We've Built:
The app we set up now allows us to show certain elements only to a select group of users. Using Permit policy editor UI, we can set a policy like this:
This is what our app looks like with the Viewer role having access to view all elements:
If we want to change this, we can change the policy in Permit’s UI (Or API). The changes to our app will take effect and deploy automatically:
Here we can see that the Viewer role can only see the “Topics for you” section:
What’s next?
Integrating Attribute-Based Access Control (ABAC) and Feature Flagging with Permit.io provides a powerful way to create personalized and secure user experiences in your applications. By following the steps outlined in this guide, you can dynamically display UI components based on user roles and attributes, ensuring that each user sees only the content they are authorized to access. This approach not only enhances security and scalability but also improves user satisfaction by delivering a tailored app experience.
Got questions? Need more guides? Want to learn more about Authorization? Join our Slack community, where there are hundreds of devs building and implementing authorization.
Written by
Daniel Bass
Application authorization enthusiast with years of experience as a customer engineer, technical writing, and open-source community advocacy. Comunity Manager, Dev. Convention Extrovert and Meme Enthusiast.