Building and Testing App Permissions with Cypress
- Share:
Intro
Handling permissions properly is a crucial part of every application. Whether you are developing a mobile app, API service, or infrastructure servers, you want to avoid finding yourself with a user who can perform an operation they are not allowed to.
Permission management is a critical component of developing secure and functional applications. By implementing one of the permissions models, such as Role-Based Access Control (RBAC) or Attribute-Based Access Control (ABAC) in applications, we ensure that only authorized users can access specific resources and features.
In this article, we'll show you how to build a resilient permission model in a to-do application by using Permit.io as the authorization as a service solution and Cypress to verify the end-to-end functionality and reliability of the model. We chose this combination of tools mainly due to their popularity in their respective categories, and their custom code minimal footprint.
Setting up a demo project
This tutorial assumes that you have installed a Node/npm environment and Docker on your machine
To demonstrate our solution, we will create a NextJS starter project in our local environment and build a classic to-do application. As part of our project, we already created a starter template that includes a basic todo app. Run the following command in your main development folder to get it on your local machine.
npx create-next-app@latest permit-todo --use-npm --example https://github.com/permitio/permit-next-todo-starter cypress-tutorial && cd permit-todo
Once the command has finished installing, open your preferred code editor in this folder, and let's see what we got.
This application starts with a small backend handler that will be responsible for Create, Read, Update, Delete, and Patch tasks. It can be found in the pages > api
folder in tasks.ts
, a fairly simple file that is responsible for all the operations in one handler function.
A single file frontend uses the API to display tasks on the other side of the app. Users can perform CRUD operations and mark tasks as completed using this app. The code can be found under the pages folder in a file called index.tsx
Now lets run npm run dev and view our todo application in the browser running on our local machine.
Permissions Model
The basic to-do app has no policy or permission model, meaning anyone can do anything. However, in a real-world scenario, we need things to act differently with a strict policy to protect the app. For example, we need a policy like: some user roles may only be able to view tasks, while others may also mark them as complete, and only administrators are allowed to delete tasks.
To begin, we will implement one of the simplest permission models, Role Based Access Control (aka RBAC). In RBAC, we assign roles to each identity that uses our app and according to this role, get a policy decision on what they can do in the application.
While enforcing policy, we analyze three dimensions that help us make a policy decision on the desired permissions model. These dimensions are Identity, Role, and Action (IRA). To design the permission model in our application, we will look at the enforcement points (where we enforce the policy decisions), and use these three dimensions to create the model.
We need to know the app's potential Identities, Roles, and Resources to create the model. It is easiest to create a simple table with the desired permissions.
Action 👉 Resource 👇 | Create | Update | Mark | Delete | Read |
---|---|---|---|---|---|
Task | Admin Editor | Admin Moderator | Admin User | Admin | Admin Moderator Editor User |
Understanding the Identity, Resource, and Action components of the enforcement point (the actual application where we ask for permission decisions) is helpful before testing these permissions. As a result, we can develop tests that verify that authorization is performed correctly for each role, ensuring that our to-do app's tasks are private and secure.
Configuring Permissions
When starting with simple RBAC, the typical approach is to write a simple code that gets the decision. It could work, but it has some problems. The main one is when we want to grow, and the RBAC code quickly becomes a spaghetti code in the application. Saas authorization services, such as Permit.io, solve this problem by decoupling the policy from the code and letting you add effortless checks where you need them in the code.
Permit.io also provides an SDK for easy implementation in the application, and their basic plan is free. Let’s start by log in to their application at app.permit.io to start configuring the permissions.
After you logged in we could start create relevant resources and actions for our IRA design. For now, there is only one resource, called Task. To keep things simple, we'll just use the HTTP methods we'll use in our application for the action.
Go to the Policy page and click Create > Resource
Create the following resource and actions.
Next, we will discuss our identities and their roles. For that we will first create different roles in our application.
Go to Policy page and click Create > Role
Add the following roles
To complete our identities, we need to create users in the system. In the real world, we may use our identity management APIs to sync users with Permit, but for now, let's just add one user per role.
Go to Users screen
Create one user per role
Now that we have configured our IRA table, we need to initialize the Policy Decision Point container. Our container is meant to be used as a lean decision point that can be used alongside any of our applications. As of now, it will run locally. Go to Permit's Connect
page. This page shows the command that needs to be executed on the local machine. Let's run it and see if it works.
Implement Permission Checks
Permit's official SDK is the recommended way to integrate Permit into an application. In Nodejs we can install it by running:
npm install –save permitio
You can get the permit SDK key from the website by going to the Projects page and choose the Copy API Key option from the three dots menu.
To avoid mistakenly pushing this secret key to a remote repository, we'll use it as an environment variable on a file that's ignored by git. Then create a new file in the main directory called .env.local and paste the following KEY=value
there:
PERMIT_SDK_KEY=<your_copied_sdk_key>
To make Permit SDK available for our checks, we will initialize the Permit SDK with our key. In an actual application, we will make it accessible globally, but for now, let’s add it to the top of the tasks.ts
file so we can use it in the APIs. Paste the following code just below the imports.
import { Permit } from 'permitio';
const permit = new Permit({
// We’ll use a cloud hosted policy decision point
pdp: "http://cloudpdp.api.permit.io/",
// The secret token we got from the UI
token: process.env.PERMIT_SDK_TOKEN,
});
Permit's SDK enables us to check for specific rules in our IRA table using an async function call. To check if our admin is allowed to perform a GET operation, we will ask the SDK:
permit.check(‘admin@todo.app’, ‘get’, ‘Task’)
This simple check uses a hard-coded email because our application demonstrates specific roles in a short list of users. In production, we will use our authenticated users' JWT (or any other auth token) instead of just emails. Using tokens also helps us send the right roles to decouple the check from the identity and roles config.
Having already called our resources in the method name, we can check for all handler operations in one place. Let's go to our tasks.ts
file and paste the following code that will first check for the user’s existence in the headers (authentication) and then adds the permit “magic” check function for authorization:
const { user } = req.headers;
if (!user) {
res.status(401).json({ message: 'unauthorized' });
return;
}
const isAllowedForOperation = await permit.check(
user as string,
req.method?.toLowerCase() as string,
'Task'
);
if (!isAllowedForOperation) {
res.status(403).json({ message: 'forbidden' });
return;
}
Now, when we will return to the application and choose a different user than admin we will see the relevant permission restrictions take effect as a result of the permit’s check that we implemented.
Try to choose any user who is not an admin,and create a task to see the full power of Permit. Did you see what happened? That's right, you've been blocked. What happens if you change the user to editor@permit-todo.app? You got it, right? :)
Add Tests
In order to ensure continuous stability of our permission model, it is time to add tests that verify that any code changes will not break it. Assuming we have some tests on the backend for the IRA functionality, it's time to conduct an end-to-end test, and we'll be using Cypress for this.
It's okay if you've never encountered Cypress before. You can test our application by running npm run cypress:open since we have already set up the testing environment in the starter project. In any case, we highly recommend taking a quick glance at their getting-started guide to understand the general concepts.
Client side e2e tests are critical to ensure the complete flows are working as expected. This is both from the client side, where partial permissions enforcement is often present (as in our app, we only show errors but do not limit views) as well as how we connect the backend flow and enforcement to our frontend screen. During permissions testing, we want them to ensure that no permissions are broken.
Add a Test for Every Occasion
Since our simple application focuses on the impersonation feature that helped us demonstrate Permit.io’s permissions model, we can go directly on to the permissions testing we would like to perform.
To create a habit of constantly verifying permissions, let’s start with creating a file called cypress/e2e/permissions.cy.ts
as a centralized place where we verify the proper permissions for each user.
By looking at this file, you can see that we created an iteration for each kind of user we have in our application and verified the CRUD operations they can do. For the example below, we are running verification that a Moderator can only edit tasks but not create them:
// Try to fail in add task
addTask('Learn Cypress and Next.js');
cy.contains('forbidden').should('exist');
cy.get('[data-testid="CloseIcon"]').parent().click();
cy.contains('Learn Cypress and Next.js').should('not.exist');
// Try to edit task
editTask('Learn Cypress', 'Learn Cypress and Next.js');
cy.contains('Learn Cypress and Next.js').should('exist');
Now that we have this file, we can always refer our developers to it; and create permission verifying tests for every occasion. We could also add it to the code-review process and verify that every code change related to permissions has a proper test case in the permissions.cy.ts
file.
Here is a recording example of the complete impersonation tests:
Next Steps: Cypress Commands
One of the features I like the most in Cypress is Commands, a small and granular test logic that we can access from anywhere to perform and verify repetitive application behavior.
To make our tests even better and more robust, we could create a bunch of authorization Cypress commands so our users could perform permission behavioral checks from anywhere. Look at the following code, for example:
// This command verify that the error message is displayed
// Run it from everywhere with cy.verifyForbiddenError()
const verifyForbiddenError = () => {
cy.contains('forbidden').should('exist');
cy.get('[data-testid="CloseIcon"]').parent().click();
}
In this command, we are verifying that an action performed by the user failed and they got a “forbidden” message. Just think of the simplicity of having it easily accessible from anywhere and letting developers make the relevant tests just in time.
This sample command is in our project's /cypress/support/commands.ts
file.
Conclusion
In this article, we covered the process of implementing permissions into an example to-do application and testing it end-to-end using Cypress. By following these step-by-step instructions, you should now have a solid understanding of how to implement and verify that permissions are working correctly in your own applications.
Adding permissions is an essential component of building secure and user-friendly applications. By using Cypress for end-to-end testing, you can ensure that your application is functioning as expected and that user data is protected. With this knowledge, you have the tools to create robust and secure applications that meet the needs of your users.
Want to learn more about implementing authorization? Got questions? Reach out to us in our Slack community.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker