How to Implement Role-Based Access Control (RBAC) in Vue.js
- Share:
Vue.js is a performant web framework for building lightning-fast web apps. It uses Vite for tooling and compilation, which makes Vue websites function at turbo speeds, giving users an awesome experience. Vue also gives builders a good user experience with Single-File Components (SFCs). SFCs allow you to declare script, template, and styles for a Vue component neatly in the same file.
When you build and deploy apps with Vue, different users will access it for various purposes. Depending on your application logic, you have to segregate what each user can do and ensure system security - that’s where authorization comes in.
Role Based Access Control (RBAC) allows you to grant or deny Vue permissions based on predefined application roles. These can correlate to organizational titles or responsibilities in your app.
This blog will guide you through implementing RBAC in Vue using Permit.io - an authorization-as-a-service solution. You'll learn how to define roles, configure permissions, and enforce access control, all while building a simple food delivery app as a practical example.
To get us started, let’s plan our RBAC implementation:
Planning our RBAC Implementation
To configure RBAC, we need to do the following:
- Outline system Resources and the Actions “performable” on them
- Define Roles and specify which Actions their holders can have on which Resources
- Assign Roles to users and enforce access control
Let’s say we are building a food delivery app. In this example service, a customer can place an order of various cooked meals with a vendor. A rider can then pick up the order from the vendor and, in turn, deliver it to the customer. The food delivery could also have an admin somewhere who oversees the system and assigns riders to orders.
As you see, we can easily outline the resources as meals
and orders
.
The roles are customer
, rider
, vendor
, and admin
.
Now let’s see how should be able to do what:
Meals: Have the actions read
, create
, update
, and delete
.
- All roles can access (
read
) meals. - Only
vendor
s andadmin
s cancreate
,update
, anddelete
meals.
Orders: Have the actions read
, create
, fulfill
, assign-rider
, and deliver
- All roles can access (
read
) orders. customer
s cancreate
orders.rider
s candeliver
orders.vendor
s canfulfill
orders.admin
s canread
,create
,fulfill
,assign-rider
, anddeliver
orders.
Defining roles, resources, and actions, as we did above, is the first and most crucial step in configuring RBAC. This step is critical for authorization setup in any platform (Vue including).
With our overall role, action, and resource mapped out, before diving into role assignment, let’s go over the application code and see where this setup comes in:
Example Vue Application Overview
The Vue codebase for this tutorial can be found here.
This example project uses tailwindcss for styling and PrimeVue for components.
It contains a simple interface with navigation for the various food delivery service roles and the actions they can perform on them. Clone it with Git and check it out!
The navigation is built with the Vue router and a custom AppMenu.vue
component. The router is created in the src/router/index.ts
file. It links to each of the four pages for each role and has a catch-all route that redirects visitors to the customer page.
// In src/router/index.ts
import AdminView from '@/views/AdminView.vue';
import CustomerView from '@/views/CustomerView.vue';
import RiderView from '@/views/RiderView.vue';
import VendorView from '@/views/VendorView.vue';
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/customer',
name: 'customer',
component: CustomerView
},
{
path: '/rider',
name: 'rider',
component: RiderView
},
{
path: '/vendor',
name: 'vendor',
component: VendorView
},
{
path: '/admin',
name: 'admin',
component: AdminView
},
{
path: '/:catchAll(.*)',
redirect: '/customer'
}
]
});
export default router;
The AppMenu.vue
component is exported from the src/components/AppMenu.vue
file. It contains a simple nav
HTML element with links to children. Each child is a route from the Router. We also added unique icons with PrimeVue to each link:
// In src/components/AppMenu.vue
<script setup lang="ts">
import { useDrawerStore } from '@/stores/drawer';
const drawer = useDrawerStore();
</script>
<template>
<nav>
<ul class="mt-2">
<li>
<router-link
to="/customer"
class="text-lg px-4 py-2 rounded-md block p-ripple"
v-ripple
@click="drawer.close"
>
<i class="pi pi-shopping-cart mr-3"></i>
<span>Customer</span>
</router-link>
</li>
<li>
<router-link
to="/rider"
class="text-lg px-4 py-2 rounded-md block p-ripple"
v-ripple
@click="drawer.close"
>
<i class="pi pi-truck mr-3"></i>
<span>Rider</span>
</router-link>
</li>
<li>
<router-link
to="/vendor"
class="text-lg px-4 py-2 rounded-md block p-ripple"
v-ripple
@click="drawer.close"
>
<i class="pi pi-warehouse mr-3"></i>
<span>Vendor</span>
</router-link>
</li>
<li>
<router-link
to="/admin"
class="text-lg px-4 py-2 rounded-md block p-ripple"
v-ripple
@click="drawer.close"
>
<i class="pi pi-wrench mr-3"></i>
<span>Admin</span>
</router-link>
</li>
</ul>
</nav>
</template>
<style scoped>
nav .router-link-active {
font-weight: bold;
}
</style>
The AppMenu component is responsive. On mobile screens, it opens from a drawer. In wider screens, it is fixed to the left of the view. Concerning the drawer, the @click
handler of every link closes the drawer. This is to give app users a good user experience.
Each of the four pages has its own functionality, as described below. Regardless, they all share the OrdersDisplay.vue
component, which is defined in src/components/OrdersDisplay.vue
. This component does just as its name says - it displays orders. It uses accordion-related components from PrimeVue to give an "expand and collapse" UI for order details.
// In src/components/OrdersDisplay.vue
<script setup lang="ts">
// ...
</script>
<template>
<p v-if="orders.all.length == 0" class="text-center opacity-30 mt-4 mb-8">
No Orders Yet.
</p>
<Accordion :value="orders.all.map(({ id }) => id)" multiple>
<AccordionPanel
v-for="order of orders.all"
:key="order.id"
:value="order.id"
>
<AccordionHeader>
<div class="grow flex justify-between mr-4">
<h3>Order #{{ order.id }}</h3>
<span> {{ order.totalPrice }} đź’µ </span>
</div>
</AccordionHeader>
<AccordionContent>
<!-- ... -->
</AccordionContent>
</AccordionPanel>
</Accordion>
</template>
The Customer page has a Cart tab for selecting Meals and creating Orders and an Orders tab for viewing Order details. The rider page only has the Orders list for delivering orders. The Vendor page has an Orders tab for viewing and fulfilling orders and a Meals tab for managing Meals. The admin page has both Orders and Meals tabs. Admins can execute all actions on each Order.
Now that our code is ready, let's configure RBAC for this app in Permit.io.
Permit.io is an Authorization-as-a-service solution that helps you focus on building core parts of your app and handles permissions for you. With Permit, you can easily create Resources, specify their Actions, and configure Roles.
To use Permit.io, we need to set up a Project (or Workspace).
How to set up a Project in Permit.io
To set up a workspace in Permit:
- Head to app.permit.io
- Sign In or Create your Account
- Create a Workspace or a Project:
If this is your first time here, you will see the “Create Workspace” button. This will auto-create a default project for you with default development and production environments.
If you already have an existing Workspace, instead, create a new project for what you are building (or this food delivery of this tutorial) within your workspace. You can create a new Project from the “Projects” menu option in the Permit.io console UI. Creating a project also auto-creates the default dev and prod environments.
- Add Project Resources and their Actions(These will be the
Meals
andOrders
as described above):- Enter the development environment.
- Navigate to its Policy Editor.
- You will need to create a Resource. When creating “Meal”, you will see that the four actions (create, read, update, and delete) are already pre-populated for you. However, when creating “Order”, remove “update” and “delete” from the default actions and add “fulfill”, “assign-rider”, and “deliver” actions.
- Add Roles:
- These will be customer, vendor, rider, and admin.
- Permit has auto-created some default roles for us (admin, editor, and viewer).
- Delete the editor and viewer roles and create the ones we are using.
- Adjust their permissions on the resources by checking the respective action in the policy table.
- Save the changes. (You can also toggle the view of the policy editor if you prefer a different view).
Abstracting authorization this way is a unique value proposition from Permit.io. If we were to build permissions ourselves, we would spend much time coding authorization repetitively which we can outsource as we are doing right now.
So, we’ve built our Vue codebase and have set up a Permit for our application. What’s Next? Well, now, we have to assign roles.
RBAC Role Assignment
To assign roles in Permit, you can use the API, any of the SDKs, or the console UI. They are all well-equipped for assigning roles. They also allow you to programmatically assign roles to users as they sign up in your application.
When using either the API or SDKs, you need the Permit token of your environment. Obtain it from the project settings in the console UI and follow the settings follow the instructions here.
Permit’s console UI is another place where you can assign roles to users in your application. It also makes it easy for you to quickly verify the roles you’ve assigned programmatically. In addition, the UI provides a seamless way for other team members in your project to do role assignments without coding experience. This is especially useful when you want to give some non-technical users the ability to control roles within your application (i.e. product managers).
Whatever option we are using to assign roles to users, we need to make Permit aware of the different users in our application and provide them with unique user identifiers. These identifiers (like user IDs or emails) will most likely be directly from the project’s database.
In this tutorial, we will use the console UI to assign users roles. Furthermore, our users will be imaginary. We will use simple IDs for them and directly hardcode them in the Vue source code.
The user IDs are customer1
, rider1
, vendor1
, and admin1
. In a production environment, you will obviously use real users here. You can learn more about how to sync your users into Permit.io here.
In the console UI, go to user management from the “Directory” navigation option, create the users, and assign their roles.
So far, we’ve created resources (meals and orders), their actions, and the roles that manage them. Additionally, we have added dummy users and assigned them roles. Now, let’s complete the authorization process and ensure that only users with appropriate roles can access resources.
How to Enforce Authorization Checks
To enforce authorization with Permit, use the Permit.check()
function, available in all Permit SDKs. It takes the user's identity, the target resource, and the action to execute and returns a boolean value (true or false) indicating whether the user is authorized.
We can’t do such logic check in the frontend, otherwise, our system will not be secure. For this reason, we need a backend to enforce authorization. In production, you will integrate Permit into your system’s backend. For this tutorial, we will use a little NodeJS/Express setup to achieve RBAC enforcement and check Vue permissions.
The backend code is located in the backend
directory alongside the Vue files in the tutorial repo. The backend is responsible for creating and managing meals and orders in the Vue code.
The Vue pinia stores make API calls to the backend when accessing meals and orders. Successively, the backend handles API routing and uses JSON files for a temporary database.
Most importantly, the backend contains middleware that allows us to verify Vue permissions. This is where we use Permit to ensure that the rightful user can execute target actions. To initialize Permit, we create a new instance with the public Cloud PDP and our Permit token.
const permit = new Permit({
pdp: '<https://cloudpdp.api.permit.io>',
token: process.env.PERMIT_TOKEN
});
The Permit token is from the local environment. Provide it by creating a .env
file in the backend directory, grabbing your Permit token from the console, and pasting in .env
as follows:
PERMIT_TOKEN=*paste-token-here*
Now that we have created a Permit instance for the middleware, let’s validate the Vue permissions. To check the permissions, we need 3 arguments: user
, action
, and resource
. For the sake of demonstration, we are getting the user from the HTTP request header from Vue (set it in the Pinia stores). For the action and the resource, we are extracting them from the HTTP request path.
Checking for permissions is as easy as calling permit.check
:
const permitted = await permit.check(user, action, resource);
Permit will check if the user (or the user’s role) has permission to complete the action on the resource. We then use the result of the check function to either allow Express to continue execution or to stop the HTTP call with an unauthorized response.
This way of checking permissions is beautiful in that it allows us to use one middleware for multiple API endpoints. It allows us to spend less time building Vue permissions and focus on other parts of the code.
Let’s get back to Vue and complete the setup.
Integrating RBAC in Vue Frontend
Integrating RBAC in a front end ensures that only authorized users have access to role-sensitive UI parts of your system. It also involves showing UI info or controls (like forms or buttons) only if the currently authenticated user has permission to view or act on them.
When integrating RBAC in Vue, we need to know the user's role in the Vue code. With that information, we can show or hide UI controls based on permissions. In production, you will use CASL for this. Read more at How to Solve Authorization in Vue with CASL.
In the example food delivery code, we kept each role on its page for demonstration and simplicity. Thus, we know what to show based on the current page. Also, the OrdersDisplay Vue component shows actions on Orders depending on hardcoded props (admin, rider, or vendor). It also handles the appropriate action following these.
Remember that this is for demo and testing purposes only.
// In src/components/OrdersDisplay.vue
<script setup lang="ts">
const { admin, rider, vendor } = defineProps<{
admin?: string;
rider?: string;
vendor?: string;
}>();
// ...
const fulfill = async (orderId: number) => {
if (vendor ?? admin) {
// ...
}
};
const assignRider = async (e: FormSubmitEvent, orderId: number) => {
if (e.valid && admin) {
// ...
}
};
const deliver = async (orderId: number) => {
if (rider ?? admin) {
// ...
}
};
</script>
<template>
<!-- ... -->
</template>
In the Vue frontend of the example food delivery, when you take an action as a given role, Vue makes an API call to the backend. The API call passes through the permissions middleware (where we added the Permit checks).
Ordinarily, the calls are successful because of this simple emulated setup. At this point, we can test permissions or see denials in action. To do this, update the user ID on a given page to an authorized equivalent, attempt a call, and see it fail.
If you set the user ID of the RiderView to customer1, then you can’t deliver orders. If you set the user ID of the AdminView to vendor1, then you can’t assign riders, and so on.
Summary
Role-Based Access Control (RBAC) is a key authorization model that helps manage user permissions based on predefined roles. In this tutorial, we explored how to implement RBAC in a Vue application using Permit.io.
We started by defining system resources, actions, and roles using a food delivery app as an example. We then configured RBAC by assigning roles to users and enforcing access control both on the frontend and backend. On the backend, we integrated Permit.io to handle permission checks efficiently, ensuring secure access to system resources. On the frontend, we structured Vue components to reflect role-based UI behavior.
By following this guide, you can integrate RBAC into your Vue projects, ensuring a scalable and secure authorization system. For more advanced use cases, combining roles with attribute-based access control (ABAC) or relationship-based access control (ReBAC) can further refine access management as your application grows.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker