Relationship-Based Access Control (ReBAC) and Attribute-Based Access Control (ABAC) are advanced authorization models beyond traditional role-based approaches. While Strapi's built-in system is great for basic role-based permissions, today's applications tend to need more sophisticated control that allows users to have access based on the relationships existing between users and resources, as well as dynamic attributes.
In this tutorial, you'll learn how to:
- Set up permissions that depend on user relationships and the resources they access in Strapi
- Create attribute-based rules considering contexts like time, location, and resource state
- Combine ReBAC and ABAC to build a more thorough access control system
- Implement these controls in both the Strapi backend and Next.js frontend
We'll demonstrate this concept by building a Blog platform, where access to posts is determined by relationships (like author, editor, or viewer) and factors such as post status, user subscription level, and content restrictions.
ReBAC and ABAC Demo
Below is a demo of how the ReBAC and ABAC roles we'll be enforcing throughout this tutorial will work in our blog application.
GitHub Project Repo
The code for this tutorial is available in my GitHub repository. The complete frontend code is in the complete-frontend
branch, and the full backend code is in the master
branch. Feel free to clone and follow along.
Prerequisites
You will need the following to get started:
- Node.js v18 or later
- A code editor on your computer
- Python
- Docker installed on your computer
What are ReBAC and ABAC?
Before we move further, let's understand what ReBAC and ABAC policies are:
Relationship-Based Access Control (ReBAC)
Relationship-Based Access Control (ReBAC) is a way to manage access by focusing on the connections between people and resources. Rather than just looking at someone's role or identity, ReBAC determines what users can access based on their relationships with others in the system. Think of it like a web, where each person or resource is linked, and those links determine what you can do or view.
For example, on LinkedIn, you can see certain posts or updates because you're connected to someone, whether directly or through shared groups. ReBAC works the same way, deciding access based on these types of connections.
Attribute-Based Access Control (ABAC)
Attribute-Based Access Control (ABAC) is an authorization model that evaluates multiple characteristics (attributes) of users, resources, actions, and environmental conditions to determine access permissions.
For example, in a blog platform, if someone wants to read a premium article, ABAC doesn't just check if they have a "subscriber" badge. Instead, it evaluates multiple factors, such as whether their subscription is still active. Are they allowed to access content in this region? Have they exceeded their monthly article limit as a free user? For instance, a free user might be allowed to read 5 articles per month, while a basic subscriber can read unlimited articles but can't access premium content, and a premium subscriber gets full access to everything including exclusive content and commenting privileges.
Setting Up the E-Learning Platform
To get started with our implementation, let's create a new Strapi project and clone our Next.js frontend starter project:
# Create Strapi project
npx create-strapi-app@latest backend
git clone https://github.com/icode247/blog-frontend
# Install Permit.io in the Strapi project
cd elearning-backend
npm install permitio
Setting up local Permit.io Policy-Decision-Point (PDP) container
A Policy-Decision-Point (PDP) is a network node responsible for answering authorization queries using policies and contextual data.
To get started, let's set up a new Permit.io project and get our API key.
Permit.io is an authorization-as-a-service tool that allows you to easily implement scalable relationship-based access control (RBAC) and attribute-based access control (ABAC) permissions in your Strapi and Next.js project without needing to modify your code when your authorization requirements change.
Create a Permit.io PDP Environment
To setup a PDP container, create a Permit.io account and set up a new project named Blog in the dashboard.
Get Permit.io API Key
Permit.io automatically provides Production and Development environments for each project. Choose your desired environment or create a new one, then copy the API key.
Pull Permit using Docker
Next, to run the Permit's ReBAC and ABAC policies, you need to set up your own local PDP container. Do that by running the command below:
docker pull permitio/pdp-v2:latest
docker run -it -p 7766:7000 \
--env PDP_DEBUG=True \
--env PDP_API_KEY=<YOUR_API_KEY> \
permitio/pdp-v2:latest
If you do not have Docker installed yet, click here to install Docker. Because you need it for the above command.
Replace <YOUR_API_KEY>
with your Permit.io API Key you copied earlier and press Enter key to run the command.
Initialize Permit Client
Next, set up Permit.io in your Strapi project by creating a permit
folder inside the backend/src
directory. Inside this folder, add a new file called backend/src/permit/client.ts
and initialize the Permit.io client with the code snippets below:
1import { Permit } from "permitio";
2
3export const permit = new Permit({
4 pdp: process.env.PERMIT_PDP_URL || "http://localhost:7766",
5 token: process.env.PERMIT_SDK_KEY || "",
6});
The above code starts the Permit.io SDK to enable fine-grained authorization systems through policy evaluation done by a Policy Decision Point (PDP) server that references defined policies.
Then, add the following helper functions to the backend/src/permit/client.ts
file to check user permissions, sync your users within Permit.io, and sync resource relationships.
1export interface PermissionContext {
2 user: {
3 key: string;
4 attributes: {
5 subscription_tier: string;
6 is_premium: boolean;
7 articles_read: number;
8 location?: string;
9 };
10 };
11 resource: {
12 type: string;
13 instance: string;
14 attributes?: Record<string, unknown>;
15 };
16}
17
18export async function syncUser(user: {
19 id: string;
20 email: string;
21 firstName?: string;
22 lastName?: string;
23}): Promise<void> {
24 try {
25 const synced = await permit.api.syncUser({
26 key: user.id,
27 email: user.email,
28 first_name: user.firstName,
29 last_name: user.lastName,
30 });
31
32 console.log("User synced:", synced);
33 } catch (error) {
34 console.error("User sync failed:", error);
35 throw error;
36 }
37}
38
39export async function assignResourceRole(
40 userId: string,
41 role: string,
42 resourceType: string,
43 resourceId: string
44): Promise<void> {
45 try {
46 await permit.api.assignRole({
47 user: userId,
48 role: role,
49 resource_instance: `${resourceType}:${resourceId}`,
50 tenant: "default",
51 });
52 } catch (error) {
53 console.error("Role assignment failed:", error);
54 throw error;
55 }
56}
In the above code snippet, we have created two helper functions that will be reused throughout our application:
assignResourceRole
to assign resource roles to given resources using thepermit.api.assignRole
method, which takes theuserId
or any unique identifier,resource_instance
, androle
andtenant
as arguments.syncUser
to have a copy of your user data required for enforcing the permissions on Permit.io
Setting up Strapi Content Types
Before implementing the permission system, we need to set up our content types in Strapi to match our authorization model.
From your Strapi admin dashboard, navigate to the Content-Type Builder page and create a new Blog-post collection type with the following fields:
Attribute | Type | Required | Default | Additional Info |
---|---|---|---|---|
title | string | Yes | - | - |
content | richtext | Yes | - | - |
type | enumeration | Yes | free | Options: free , premium , members-only |
status | enumeration | Yes | draft | Options: draft , published , archived |
region_restrictions | json | No | - | - |
author | relation | No | - | Relation: manyToOne → plugin::users-permissions.user (inversed by blog_posts ) |
editors | relation | No | - | Relation: manyToMany → plugin::users-permissions.user (inversed by edited_posts ) |
comments | relation | No | - | Relation: oneToMany → api::comment.comment (mapped by blog_post ) |
Create another collection type for Comment with the following fields:
Attribute | Type | Required | Default | Additional Info |
---|---|---|---|---|
content | text | Yes | - | - |
author | relation | No | - | Relation: manyToOne → plugin::users-permissions.user (inversed by comments ) |
blog_post | relation | No | - | Relation: manyToOne → api::blog-post.blog-post (inversed by comments ) |
status | enumeration | No | pending | Options: pending , approved , rejected |
Lastly, create another one for Subscription Content Type and add the following fields:
Attribute | Type | Required | Default | Additional Info |
---|---|---|---|---|
tier | enumeration | Yes | free | Options: free , basic , premium |
is_premium | boolean | Yes | false | - |
expires_at | datetime | No | - | - |
user | relation | No | - | Relation: oneToOne → plugin::users-permissions.user (inversed by subscription ) |
Configuring Users & Permissions
Let's use Strapi's built-in RBAC (Role-Based Access Control) policy to configure user access for each of the collections we've created.
Strapi provides two default roles for managing access:
- Public – for resources that are publicly accessible.
- Authenticated – for protected resources that require user authentication.
To configure permissions, navigate to: Settings → Users & Permissions plugin → Roles
Click on each role and grant users access to the collections as needed. For this demonstration, we’ll be working with the Authenticated role.
Grant the Authenticated role full access to the following collections:
- Blog Posts – Allow users to create, read, update, and delete their posts.
- Comments – Permit users to add comments and view others' comments but restrict them from modifying or deleting comments they don’t own.
- Subscriptions – Allow users to view and manage their subscription details.
These permissions will ensure that authenticated users can interact with the platform effectively while maintaining security and control.
Later, we’ll implement ReBAC (Relationship-Based Access Control) and ABAC (Attribute-Based Access Control) to introduce more advanced, dynamic permission rules based on user roles, relationships, and attributes.
Configuring Permit.io for ReBAC and ABAC
With the initial role-based permissions in place,let's proceed to implementing a we have a more dynamic and fine-grained authorization.
Before writing any application code, let's configure our authorization model in Permit.io to support ReBAC and ABAC.
Configuring ReBAC in Permit.io
Let's set up our authorization model in Permit.io. We'll start by configuring ReBAC for our blog platform.
Step 1: Modeling our System First, let's map out the resources and relationships we need to manage access to:
Our platform revolves around two key resources: Posts and Comments. A Post is the main content, and a Comment is a response tied to a post.
To manage access, users have different roles:
- Authors create posts.
- Editors review and update content.
- Subscribers access premium posts.
- Moderators manage comments.
- Viewers can only read content.
Each role has specific actions like viewing, editing, deleting, or commenting, that determine what they can do. Since posts and comments are connected, permissions follow relationships. Using ReBAC (Relationship-Based Access Control), we define who gets access based on their role and connection to the content.
Step 2: Setting Up Resources and Actions 1. Navigate to Policy → Resources from your Permit.io dashboard. 2. Click on Add Resource and create these resources in order:
Create a BlogPost resource with the following actions:
read
- View blog post contentedit
- Edit blog post contentdelete
- Delete blog postcomment
- Add comments to a blog post
Create a Comment resource with these actions:
read
- View commentsedit
- Edit comment contentdelete
- Delete commentmoderate
- Approve or reject comments
Step 3: Mapping Resource Roles For each resource, set up resource roles under "ReBAC Options":
- For BlogPost resource, add:
author
- Full control over their postseditor
- Can edit and moderate postssubscriber
- Can view and comment on posts
- For Comment resource, add:
author
- Can edit and delete their commentsmoderator
- Can approve, reject, and delete commentsviewer
- Can only view comments
- Configure permissions for each role in the Policy Editor:
Step 4: Setting Up Resource Relationships Now define the relationships between BlogPost and Comment resources:
- Open the BlogPost resource to edit
- Under Relations, set up:
Step 5: Setting Up Role Derivations Configure how roles are derived based on relationships:
- Navigate to the Roles tab
- Under ReBAC Options, set up derivations:
For BlogPost Author derivation:
- When a user is a
BlogPost#author
, they automatically becomeComment#moderator
for all comments on their posts
For Editor derivation:
- When a user is a
blogpost#editor
, they automatically becomecomment#moderator
for all comments on posts they can edit.
This setup follows a relationship-based pattern where permissions flow between resources based on user roles. Here's what happens:
- When a user is assigned as the author of a BlogPost:
- They get full control over their post (edit, delete, view)
- Through derivation, they automatically become a moderator of all comments on their post
- This means they can approve, reject, or delete comments without needing separate permissions
- Similarly for editors:
- When assigned as editor of a BlogPost
- They automatically get moderator permissions for comments
- This allows them to manage the discussion on posts they edit
- However, they can't delete the post itself (reserved for authors)
Adding ReBAC Policies to Strapi
With the permissions configured, let's proceed with integrating it with our Strapi application. First, create a lifecycle hook to sync users who are creating blog posts with Permit. Create a lifecycles.ts
file in the backend/src/api/blog-post/content-types/blog-post
folder and add the code snippets below:
1import { syncUser, assignResourceRole } from "../../../../permit/client";
2
3interface BlogPost {
4 id: string;
5 title: string;
6 author: {
7 id: string;
8 email: string;
9 firstName?: string;
10 lastName?: string;
11 };
12 editors?: Array<{
13 id: string;
14 email: string;
15 firstName?: string;
16 lastName?: string;
17 }>;
18}
19
20async function populateBlogPost(result: BlogPost) {
21 return await strapi.documents("api::blog-post.blog-post").findOne({
22 documentId: (result as any).documentId,
23 populate: ["author", "editors", "subscribers"],
24 });
25}
26
27async function syncUserAndAssignRole(
28 user: { documentId: string; email: string; username?: string },
29 role: string,
30 resource: string,
31 resourceId: string
32) {
33 if (!user) return;
34
35 await syncUser({
36 id: user.documentId,
37 email: user.email,
38 firstName: user.username,
39 });
40
41 await assignResourceRole(user.documentId, role, resource, resourceId);
42}
43
44export default {
45 async afterCreate(event: { result: BlogPost }) {
46 try {
47 const blogPost = await populateBlogPost(event.result);
48
49 await syncUserAndAssignRole(
50 blogPost.author as any,
51 "author",
52 "BlogPost",
53 blogPost.documentId
54 );
55
56 // if an editors were asigned to the blog post
57 if (blogPost.editors) {
58 for (const editor of blogPost.editors) {
59 await syncUserAndAssignRole(
60 editor as any,
61 "editor",
62 "BlogPost",
63 blogPost.documentId
64 );
65 }
66 }
67 } catch (error) {
68 console.error("Failed to sync with Permit.io:", error);
69 }
70 },
71
72 async afterUpdate(event: { result: BlogPost }) {
73 try {
74 const blogPost = await populateBlogPost(event.result);
75
76 if (blogPost.editors) {
77 for (const editor of blogPost.editors) {
78 await syncUserAndAssignRole(
79 editor as any,
80 "editor",
81 "BlogPost",
82 blogPost.documentId
83 );
84 }
85 }
86 } catch (error) {
87 console.error("Failed to sync with Permit.io:", error);
88 }
89 },
90};
The above code snippet will listen to the Strapi's afterCreate
and afterUpdate
events in the Blog-post collection type to sync the users that created the post with a permit and assign the appropriate roles to them in Permit.io.
Then, create a lifecycle method for the subscription collection to sync the subscribers with Permit.io to grant them access after they've subscribed. Create a lifecycles.ts
file in the backend/src/api/blog-post/content-types/subscription
folder and add the code snippets below:
1import { syncUser, assignResourceRole } from "../../../../permit/client";
2
3interface BlogPost {
4 id: string;
5 title: string;
6 author: {
7 id: string;
8 email: string;
9 firstName?: string;
10 lastName?: string;
11 };
12 editors?: Array<{
13 id: string;
14 email: string;
15 firstName?: string;
16 lastName?: string;
17 }>;
18}
19
20async function populateBlogPost(result: BlogPost) {
21 return await strapi.documents("api::blog-post.blog-post").findOne({
22 documentId: (result as any).documentId,
23 populate: ["author", "editors", "subscribers"],
24 });
25}
26
27async function syncUserAndAssignRole(
28 user: { documentId: string; email: string; username?: string },
29 role: string,
30 resource: string,
31 resourceId: string
32) {
33 if (!user) return;
34
35 await syncUser({
36 id: user.documentId,
37 email: user.email,
38 firstName: user.username,
39 });
40
41 await assignResourceRole(user.documentId, role, resource, resourceId);
42}
43
44export default {
45 async afterCreate(event: { result: BlogPost }) {
46 try {
47 const blogPost = await populateBlogPost(event.result);
48
49 await syncUserAndAssignRole(
50 blogPost.author as any,
51 "author",
52 "BlogPost",
53 blogPost.documentId
54 );
55
56 // if an editors were asigned to the blog post
57 if (blogPost.editors) {
58 for (const editor of blogPost.editors) {
59 await syncUserAndAssignRole(
60 editor as any,
61 "editor",
62 "BlogPost",
63 blogPost.documentId
64 );
65 }
66 }
67 } catch (error) {
68 console.error("Failed to sync with Permit.io:", error);
69 }
70 },
71
72 async afterUpdate(event: { result: BlogPost }) {
73 try {
74 const blogPost = await populateBlogPost(event.result);
75
76 if (blogPost.editors) {
77 for (const editor of blogPost.editors) {
78 await syncUserAndAssignRole(
79 editor as any,
80 "editor",
81 "BlogPost",
82 blogPost.documentId
83 );
84 }
85 }
86 } catch (error) {
87 console.error("Failed to sync with Permit.io:", error);
88 }
89 },
90};
Now, create an application-level middleware to enforce the ReBAC policies in your Blog-Post and Subscription routes. Create the backend/src/middlewares/rebac-check.ts
file in the src
folder and add code snippets below:
1import { Context, Next } from "koa";
2import { permit } from "../permit/client";
3import type { Core } from "@strapi/strapi";
4
5export const ACTIONS = {
6 get: "read",
7 put: "edit",
8 delete: "delete",
9};
10
11export const RESOURCE = {
12 "/comments": "Comment",
13 "/blog-posts": "BlogPost",
14};
15
16export default (_config: any, { strapi }: { strapi: Core.Strapi }) => {
17 return async (ctx: Context, next: Next) => {
18 const { user } = ctx.state;
19
20 if (!user) {
21 return ctx.unauthorized("Authentication required");
22 }
23 const postId = ctx.params.id;
24 const action = ctx.request.method.toLowerCase();
25 const resourcePath = "/" + ctx.request.url.split("/").filter(Boolean)[1];
26 try {
27 const permitted = await permit.check(
28 user.documentId.toString(),
29 ACTIONS[action],
30 {
31 type: RESOURCE[resourcePath],
32 key: `${RESOURCE[resourcePath]}:${postId}`,
33 }
34 );
35
36 if (!permitted) {
37 return ctx.forbidden("Access denied by permission policy");
38 }
39
40 await next();
41 } catch (error) {
42 strapi.log.error("Permission check failed:", error);
43 return ctx.internalServerError("Permission check failed");
44 }
45 };
46};
These code snippets will handle the ReBAC permission enforcement on the Comments and Blog-post collection using the permit.check
function, which accepts the unique identifier we used when syncing the user, the action the user tends to perform, the resource type and resource instance.
Testing the ReBAC Implementation
Now that we have set up our ReBAC policies and implemented them in Strapi let's test them to see how they work. We'll create some test data and verify our permission rules. Navigate to your Strapi dashboard and create the following users in the User collection type:
1{
2 "username": "author1",
3 "email": "author@example.com",
4 "password": "testpass123",
5 "confirmed": true,
6 "blocked": false
7},
8{
9 "username": "editor1",
10 "email": "editor@example.com",
11 "password": "testpass123",
12 "confirmed": true,
13 "blocked": false
14},
15{
16 "username": "subscriber1",
17 "email": "subscriber@example.com",
18 "password": "testpass123",
19 "confirmed": true,
20 "blocked": false
21}
Then create the new blog posts below, assign the author1 as the author, editor1 as the editor.
1 {
2 "title":"Test Blog Post",
3 "content": "This is a test blog post content",
4 "type": "premium",
5 "status": "published",
6 "author": author1,
7 "editors": [editor1],
8 "region_restrictions": ['US', 'UK']
9 }
Then, create a new subscription. Select subscribe1
as the user selects the blog post we created as the blog and premium as the plan:
After creating the above records, navigate to the Directory page in your Permit account; you'll find the users and the instance roles assigned to them:
When an editor tries to delete a comment, they will get a permission error.
The responses will show how ReBAC enforces permissions based on relationships:
- Authors have full control over their posts and comments
- Editors can edit posts but can't delete them
- Subscribers can only view and comment
You can verify these permissions in your Strapi admin panel under the Audit Logs section or by checking the Permit.io dashboard's activity logs.
Configuring ABAC Rules
Next, we'll build upon this foundation by adding ABAC rules to consider attributes like subscription status and regional restrictions...
Step 1: Define User Attributes Navigate to Directory → Users → Settings → User Attributes and add:
1{
2 "subscription_tier": {
3 "type": "string",
4 },
5 "is_premium": {
6 "type": "boolean"
7 },
8 "articles_read": {
9 "type": "number"
10 },
11 "location": {
12 "type": "string"
13 }
14}
Step 2: Define Resource Attributes For the BlogPost resource, add these attributes:
1{
2 "type": {
3 "type": "string",
4 "enum": ["free", "premium", "members-only"]
5 },
6 "status": {
7 "type": "string",
8 "enum": ["draft", "published", "archived"]
9 },
10 "region_restrictions": {
11 "type": "array",
12 "items": {
13 "type": "string"
14 }
15 }
16}
Step 3: Creating Condition Sets Navigate to Policy → ABAC Rules and create the following condition sets:
Premium Content Access
1{
2 "name": "premium_access",
3 "conditions": [
4 {
5 "user.subscription_tier": {
6 "operator": "equals",
7 "value": "premium",
8 },
9 "user.is_premium": {
10 "operator": "equals",
11 "value": true
12 }
13 }
14 ]
15}
Regional Access Control
1{
2 "name": "regional_access",
3 "conditions": [
4 {
5 "user.location": {
6 "operator": "equals",
7 "value": "resource.region_restrictions"
8 }
9
10 }
11 ]
12}
Article Reading Limit
1{
2 "name": "reading_limit",
3 "conditions": [
4 {
5 "user.subscription_tier": {
6 "operator": "equals",
7 "value": "free"
8 },
9 "user.articles_read": {
10 "operator": "less-than",
11 "value": 5
12 }
13 }
14 ]
15}
Step 4: Apply ABAC Rules In the Policy Editor, apply these rules to actions:
For BlogPost resource:
read
: Checkregional_access
AND (premium_access
ORreading_limit
)comment
: Checkregional_access
ANDpremium_access
edit
: Requiresauthor
oreditor
role +regional_access
delete
: Requiresauthor
role only
For Comment resource:
read
: Checkregional_access
edit
: Requiresauthor
role ANDregional_access
delete
: Requiresauthor
ormoderator
rolemoderate
: Requiresmoderator
role
This configuration creates a sophisticated permission system that controls access based on user relationships (author, editor, subscriber), enforces regional content restrictions, manages premium content access, implements reading limits for free users, and provides proper comment moderation controls.
Adding ABAC to Our Middleware
Next, enforce the ABAC rules we configured for your Strapi application. First, let's update your backend/src/middleware
to include ABAC checks:
1import { Context, Next } from "koa";
2import { permit } from "../permit/client";
3import type { Core } from "@strapi/strapi";
4
5export const ACTIONS = {
6 get: "read",
7 put: "edit",
8 delete: "delete",
9 post: "create",
10};
11
12export const RESOURCES = {
13 "/comments": "Comment",
14 "/blog-posts": "BlogPost",
15};
16
17export default (_config: any, { strapi }: { strapi: Core.Strapi }) => {
18 return async (ctx: Context, next: Next) => {
19 const { user } = ctx.state;
20
21 if (!user) {
22 return ctx.unauthorized("Authentication required");
23 }
24
25 const postId = ctx.params.id;
26 const action = ctx.request.method.toLowerCase();
27 const resourcePath = "/" + ctx.request.url.split("/").filter(Boolean)[1];
28
29 try {
30 const subscription = await strapi.db
31 .query("api::subscription.subscription")
32 .findOne({
33 where: { user: user.id },
34 select: ["tier", "is_premium"],
35 });
36
37 const articlesRead = await strapi.db
38 .query("plugin::users-permissions.user")
39 .findOne({
40 where: { id: user.id }
41 });
42
43 let resourceAttributes = {};
44 if (RESOURCES[resourcePath] === "BlogPost" && postId) {
45 const post = await strapi.db.query("api::blog-post.blog-post").findOne({
46 where: { id: postId },
47 select: ["type", "status", "region_restrictions"],
48 });
49
50 resourceAttributes = {
51 type: post.type,
52 status: post.status,
53 region_restrictions: post.region_restrictions,
54 };
55 }
56
57 const permitted = await permit.check(
58 user.id.toString(),
59 ACTIONS[action],
60 {
61 type: RESOURCES[resourcePath],
62 key: `${RESOURCES[resourcePath]}:${postId}`,
63 attributes: resourceAttributes,
64 },
65 {
66 user: {
67 attributes: {
68 subscription_tier: subscription?.tier || "free",
69 is_premium: subscription?.is_premium || false,
70 articles_read: articlesRead?.articles_read || 0,
71 location: ctx.request.headers["x-user-location"],
72 },
73 },
74 }
75 );
76
77 if (!permitted) {
78 return ctx.forbidden("Access denied by permission policy");
79 }
80
81 await next();
82 } catch (error) {
83 strapi.log.error("Permission check failed:", error);
84 return ctx.internalServerError("Permission check failed");
85 }
86 };
87};
First of all, this middleware checks whether the user is authenticated and, if so, enforces Attribute Based Access Control (ABAC) in a Strapi app. It decides what action to take (read
, create
, edit
, delete
) depending on the HTTP method and what resource is being accessed (BlogPost, Comment). It then gets user details, including subscription tier and articles read, as well as resource attributes like post type and region restrictions. It uses permit.check
to check if the user has permission based on these attributes. It allows the request to proceed if access is granted and returns an error if denied.
Adding ReBAC and ABAC to Next.js
Now, let's implement the frontend components that enforce these permission rules. Create the following files: an api/api.ts
file in the blog-frontend/lib
directory in your frontend and add the code snippets:
1export async function getBlogPost(id: string) {
2 const res = await fetch(`/api/blog-posts/${id}`, {
3 headers: {
4 'x-user-location': localStorage.getItem('userLocation') || '', // For ABAC location check
5 }
6 });
7
8 if (!res.ok) {
9 if (res.status === 403) {
10 throw new Error('Access denied to this content);
11 }
12 throw new Error('Failed to fetch blog post');
13 }
14
15 return res.json();
16}
17
18export async function updateBlogPost(id: string, data: any) {
19 const res = await fetch(`/api/blog-posts/${id}`, {
20 method: 'PUT',
21 headers: {
22 'Content-Type': 'application/json',
23 'x-user-location': localStorage.getItem('userLocation') || '',
24 },
25 body: JSON.stringify(data),
26 });
27
28 if (!res.ok) {
29 throw new Error('Failed to update blog post');
30 }
31
32 return res.json();
33}
The above code will handle all our API calls to the Strapi backend to fetch blogs.
Then, create and update the BlogPost in the blog-blog-frontend/components
folder to display the blogs and respect the permission rules we have defined:
1import { useState, useEffect } from "react";
2import CommentSection from "./CommentSection";
3import { api } from "@/lib/api";
4
5interface BlogPostProps {
6 id: string;
7 userSubscription: {
8 tier: string;
9 is_premium: boolean;
10 };
11}
12
13export default function BlogPost({ id, userSubscription }: BlogPostProps) {
14 const [post, setPost] = useState<any>(null);
15 const [error, setError] = useState<string>("");
16 const [isEditing, setIsEditing] = useState(false);
17
18 useEffect(() => {
19 async function loadPost() {
20 try {
21 const data = await api.getBlogPost(id);
22 setPost(data);
23 } catch (err: any) {
24 setError(err.message);
25 }
26 }
27 loadPost();
28 }, [id]);
29
30 if (error) {
31 return (
32 <div className="p-4 bg-red-100 text-red-700 rounded">
33 {error === "Access denied to this content" ? (
34 <>
35 <h3 className="font-bold">Premium Content</h3>
36 <p>This content requires a premium subscription to access.</p>
37 </>
38 ) : (
39 error
40 )}
41 </div>
42 );
43 }
44
45 if (!post) return <div>Loading...</div>;
46
47 return (
48 <div className="max-w-2xl mx-auto p-4">
49 <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
50
51 {/* Premium content badge */}
52 {post.type === "premium" && !userSubscription.is_premium && (
53 <div className="bg-yellow-100 p-2 mb-4 rounded">⭐ Premium Content</div>
54 )}
55
56 {/* Content */}
57 <div className="prose max-w-none">
58 {isEditing ? (
59 <textarea
60 value={post.content}
61 onChange={(e) => setPost({ ...post, content: e.target.value })}
62 className="w-full p-2 border rounded"
63 />
64 ) : (
65 <div>{post.content}</div>
66 )}
67 </div>
68
69 <div className="mt-4 space-x-2">
70 {post.canEdit && (
71 <button
72 onClick={() => setIsEditing(!isEditing)}
73 className="bg-blue-500 text-white px-4 py-2 rounded"
74 >
75 {isEditing ? "Save" : "Edit"}
76 </button>
77 )}
78
79 {post.canDelete && (
80 <button
81 onClick={() => {
82
83 }}
84 className="bg-red-500 text-white px-4 py-2 rounded"
85 >
86 Delete
87 </button>
88 )}
89 </div>
90
91 {/* Comments section */}
92 <CommentSection postId={id} userSubscription={userSubscription} />
93 </div>
94 );
95}
Next, update the CommentSection
in the blo to render the comments and enforce the comment moderation rules:
1import { useState } from "react";
2
3interface CommentSectionProps {
4 postId: string;
5 userSubscription: {
6 tier: string;
7 is_premium: boolean;
8 };
9 }
10
11 export default function CommentSection({ postId, userSubscription }: CommentSectionProps) {
12 const [comments, setComments] = useState<any[]>([]);
13
14 // Only premium users can comment
15 const canComment = userSubscription.is_premium;
16
17 return (
18 <div className="mt-8">
19 <h2 className="text-2xl font-bold mb-4">Comments</h2>
20
21 {/* Comment form - Only shown to premium users */}
22 {canComment ? (
23 <form className="mb-4">
24 <textarea
25 placeholder="Add a comment..."
26 className="w-full p-2 border rounded"
27 />
28 <button
29 type="submit"
30 className="mt-2 bg-blue-500 text-white px-4 py-2 rounded"
31 >
32 Post Comment
33 </button>
34 </form>
35 ) : (
36 <div className="bg-gray-100 p-4 rounded mb-4">
37 Upgrade to premium to join the discussion
38 </div>
39 )}
40
41 {/* Comments list */}
42 <div className="space-y-4">
43 {comments.map((comment: any) => (
44 <div key={comment.id} className="border p-4 rounded">
45 <div>{comment.content}</div>
46
47 {/* Moderation controls - Only shown to moderators */}
48 {comment.canModerate && (
49 <div className="mt-2 space-x-2">
50 <button className="text-green-500">Approve</button>
51 <button className="text-red-500">Reject</button>
52 </div>
53 )}
54 </div>
55 ))}
56 </div>
57 </div>
58 );
59 }
We've successfully extended our Strapi RBAC policy and implemented and enforced both ReBAC and ABAC rules through the API, shown/hide UI elements based on permissions, handled premium content restrictions, implemented comment moderation, and considered user location and subscription status.
Each component checks permissions before rendering sensitive controls, and the API layer enforces these permissions server-side.
Testing the ReBAC and ABAC Policy
Now run your front end with the command below:
npm run dev
Then navigate to http://localhost:3000
and log in with one of the users we created earlier.
Now, you will be able to see all the blogs and the kind of blogs they are.
If a user who has not subscribed tries viewing the premium blog, they will get a warning message like in the screenshot below:
Conclusion
Throughout this tutorial, we've upgraded our basic blog platform into a robust content management system by implementing Permit.io's ReBAC and ABAC authorization. We've moved beyond Strapi's basic RBAC.
We extended the application authorization policies beyond Strapi's basic RBAC to relationship-based permissions where authors can moderate comments on their posts and dynamic attribute-based rules that consider subscription status and regional restrictions; we’ve also made content permissions based on user subscription tiers.
After this, we connected Strapi’s backend to Permit.io for permission management, added auto-syncing of user and role based on lifecycles, and made a Next.js frontend respecting these complex permissions.
This approach simplifies building your permission rules and will allow you to keep your code clean and maintainable, whether writing a blog, a learning platform, or any content-driven application.
I am Software Engineer and Technical Writer. Proficient Server-side scripting and Database setup. Agile knowledge of Python, NodeJS, ReactJS, and PHP. When am not coding, I share my knowledge and experience with the other developers through technical articles