Skip to main content
Version: 2.0.0

Pink Mobile Demo Application

Pink Mobile is a simple mobile plan management application that demonstrates the basic principles of fine-grained authorization by applying the impersonation of three different user personas: Customer, Representative, and Manager.

By utilizing this simple web application based on Node.js, you can learn how to model fine-grained authorization using Permit, and how to enforce policy decisions in your application.

Take a look at the application in action -

Modeling Fine-Grained Authorization

The first step to understanding fine-grained authorization is to find the appropriate policy model for our needs. We will use the plane model to explain the different aspects of policy modeling. Authorization can be divided into several different decoupled planes; the control plane (the configuration of the rules), the data plane (the data that helps us make decisions), and the enforcement plane (the actual enforcement of the rules). The decoupling of these plains helps us ensure that policies are streamlined and easy to manage.

In the following sections, we will go over each of the planes and see how our application handles them.

tip

While we are using the Permit Node.js SDK to configure permissions, you can also use the Permit dashboard to configure fine-grained authorization for your application.

Control Plane

The control plane is where all the policy rules and the entities required to create fine-grained rules are configured. Here's a diagram with the entities used in our application:

Control Plane Entities

info

You can view the full configuration in the setup.js file

Let's go over each of the entities and see how we configured them in our application -

Resources

The first step in protecting data is understanding which resources we wish to protect. In Pink Mobile, we have four types of resources:

  • Users: Customers who might want an agent to manage their plan
  • Representatives: An agent that can manage customers
  • Accounts: A top-level resource for everything user-related (e.g., personal details)
  • Plans: The plan used by the user

Of course, in a real mobile management application, there would be many more resources. For the sake of simplicity, we'll stick to these four.

The following code from the setup.js file is an example of setting up the Representatives resource:

await permit.api.resources.create({
key: "representatives",
name: "Representatives",
actions: {
list: {
name: "List",
},
assign: {
name: "Assign",
},
},
});

Resource Relationships

As we mentioned, the account resource is acting as a top-level resource for the user. This means the account permissions need to be propagated to all resources related to this account. For example, if a user is allowed to perform an operation on their account, they should also be allowed to perform the same operations on their plan.

To create this kind of relationship, we need to declare the relations between resources in our system. Here's an example of how we can declare the relationship between the Account and Plan resources using the Permit Node.js SDK:

await permit.api.resources.create({
key: "plan",
name: "Plan",
...
relations: {
parent: "account",
},
});

Roles

Now that we've modeled and configured the resources, we can define the roles allowed to perform operations on these resources. In Pink Mobile, we can identify the following roles:

Environment-level Roles

These roles allow users to perform operations on all the resources from a particular type in the environment.

  • Manager: Can perform some operations on all Representatives in the system
  • Representative: Can perform some operations on all Users in the system

Here's a code example of adding the Manager role to the environment using the Permit Node.js SDK:

await permit.api.roles.create({
key: "manager",
name: "Manager",
permissions: [
"representatives:assign",
"account:view",
"users:create",
"representatives:list",
"plan:view",
"users:list",
],
});

Resource-level Roles

These roles are more fine-grained, and allow to perform operations on a specific resource a user is assigned to. We will write this role in the following convention: Resource#Role

  • Account#Owner: Can perform every operation on a User's account
  • Account#Member: Can perform some viewing operations on a User's account
  • Account#Editor: Can perform some editing operations on a User's account
  • Plan#Editor: Can perform some editing operations on a User's plan

Here's a code example of adding the Account#Owner role as part of the resource configuration using the Permit Node.js SDK:

await permit.api.resources.create({
key: "plan",
name: "Plan",
...
roles: {
editor: {
name: "Editor",
permissions: ["view", "change"],
...
},
},
...
});

Role Derivations

Looking at the previous list, we can see a hierarchy in the roles, the same as we had with resources. One example is granting additional permissions on related resources, such as plans, to users with a specific role on the account. To do so, we can configure a role derivation. This will allow us to derive the permissions from the account to the plan. The role derivation will be configured as such:

Account#Editor -> Plan#Editor - every user that has the Account#Editor role, derives all the relevant permissions in the Plan#Editor role.

Here's a code example of adding role derivation to the environment using the Permit Node.js SDK:

await permit.api.resources.create({
key: "plan",
name: "Plan",
...
roles: {
editor: {
name: "Editor",
permissions: ["view", "change"],
granted_to: {
users_with_role: [
{
role: "editor",
on_resource: "account",
linked_by_relation: "parent",
},
],
},
},
},
...
});

Conditions

To create even fine-grained authorization, we can leverage our resource and user attributes to create condition sets based on our resources. Creating such conditions will give us the option to match them in the policy, and allow/deny an operation based on the condition.

User Sets

The first type of condition we can create is a user set. A condition (or set of conditions) that's based on user attributes. In our application, we'll create the following conditions:

User SetConditionDescription
Blocked Usersuser.blocked == trueA condition that checks if the user is blocked
Active Usersuser.blocked == falseA condition that checks if the user is active

Here's an example of how to create a Blocked Users condition using the Permit Node.js SDK:

await permit.api.conditionSets.create({
key: "blocked_users",
name: "Blocked Users",
type: "userset",
conditions: {
allOf: [{ allOf: [{ "user.blocked": { equals: true } }] }],
},
});

Resource Sets

The second type of condition we can create is a resource set. A condition (or set of conditions) based on resource attributes. In a resource set, we can also match user attributes with resource attributes to test things like ownership. In our application, we'll create the following conditions:

Resource SetConditionDescription
Owned Resourcesresource.owner == user.idA condition that checks if the user is the owner of the resource

Here's an example of how to create a Owned Resources condition using the Permit Node.js SDK:

await permit.api.conditionSets.create({
key: "owned_plans",
name: "Owned Plans",
type: "resourceset",
resource_id: "plan",
conditions: {
allOf: [
{ allOf: [{ "resource.owner": { equals: { ref: "user.key" } } }] },
],
},
});

Policy Rules

At this point, we have all the building blocks required to create a fine-grained policy table for our application. Looking at the Permit dashboard, this is how our application policy table should look like: Policy Editor

To set this policy up, we used the Permit API in the setup script. For example, here's where we assigned active users permissions to perform operations on their owned resources:

await permit.api.conditionSetRules.create({
user_set: "active_users",
resource_set: "owned_plans",
permission: "plan:view",
});
await permit.api.conditionSetRules.create({
user_set: "active_users",
resource_set: "owned_plans",
permission: "plan:change",
});

Data Plane

The data plane is where we set up the necessary data to enable our authorization service to make proper decisions. For example, we would want to let the policy engine know our user roles, attributes, etc. This application is designed to manage the following data plane entities:

Users

The most basic entity in every authorization data plane is the user. Their basic information and unique ID will help us match them with the resources on which they are allowed to perform operations. In a standard application, Permit's sync user function would be connected to your authentication provider. In our application, we used the Setup.js script to configure 6 users and assign each of them different roles and attributes. Here's an example of how we configured the Sirius Black user:

permit.api.users.sync({
email: "sirius@pink.mobile",
key: "sirius@pink.mobile",
first_name: "Sirius",
last_name: "Black",
attributes: {},
});

User Roles

For each user in our application, we would like to assign one or more resource or environment roles. This will help us match the user with the proper permissions in the policy table.

Here's an example of how we assigned the Representative role to the Sirius Black user:

await permit.api.roleAssignments.assign({
role: "representative",
user: "sirius@pink.mobile",
tenant: "default",
});

Here's another example of a resource-role assignment, where we assign harry@potter.io the Account#Owner role for his account instance:

await permit.api.roleAssignments.assign({
role: "owner",
resource_instance: `account:harry`,
user: "harry@potter.io",
tenant: "default",
});

Relationship Tuples

In order to ensure the role derivation is correctly deriving roles from one resource to another, we need to create relationship tuples between the resources. In our application, we used the Setup.js script to configure the relationship tuples between the resources. Here's an example of how we configured the relationship between Harry's account and Harry's plan:

await permit.api.relationshipTuples.create({
subject: `account:harry`,
object: `plan:harry`,
relation: "parent",
tenant: "default",
})

Enforcement Plane

In the enforcement plane, we will enforce the policy decisions with the configuration from the control plane and the data from the data plane. In our application, we are using the Permit Node.js SDK to enforce the policy decisions.

The Check Function

In the authorizer.js file, you'll find a function that is responsible for enforcing the policy decisions. Here's an example of how we enforce the policy decisions for the Change Plan operation:

export const authorize = async (user, action, resource) => {
return permit.check(user, action, resource);
};

The permit.check function that we are using to enforce permissions will take the current user, operation, and resource and return a decision based on the policy table that is configured in the control plane, along with the data configured in the data plane.

Data Filtering

Another way to enforce permissions is to query the Permit APIs for particular roles and data to ensure we are not exposing data that the user is not allowed to see. In our application, we are filtering the allowed representatives and their allowed users by querying the relevant roles in Permit configuration. Here's an example of how we are filtering only the users who are owners of a plan:

const owners = await permit.api.roleAssignments.list({
tenant: "default",
role: "owner",
});

Get Started

To learn more about the application itself and run it locally, you can visit the Pink Mobile Demo Repository on GitHub. If you need any help, feel free to reach out to us on our Slack channel.