Get insightful engineering articles delivered directly to your inbox.
By

— 8 minute read

Refactoring Permissions - Part 1

Welcome to Thunderdome … (refactoring important logic at scale)

Permissions affect every aspect of an application with users who have varying rights. This three part series outlines the process and the techniques we used to pull off a hat trick of decoupling crucial business logic and reducing the overall complexity of our application.

Why refactor permissions anyway?

“Can a user do an action to an object?” - that question lies at the intersection of security and user action. A wrong answer can create a security hole. A wrong answer can deny a user the rights to do something. A right answer goes unnoticed and unloved. A right answer just allows the user to go on their merry way. It doesn’t get recorded in the logs. It’s a game of screwing up or maintaining the status quo.

This situation becomes tenuous with millions of users with varying permissions. Add in a robust security model with different levels of user rights. Add in different types of organizations or settings and you have a scary problem. Now, screwing up irritates a lot of people. Screwing up creates security holes that affect millions of records. We won’t even mention audit logs - tracking down why user 12345 had the ability to delete a record. Getting right just keeps things churning along.

It’s no longer about what action a user can do to an object. It’s about what kinds of users can do what kind of action to objects. All those roles, flags, options become a puzzle. What if a user is a manager but we downgraded their account to read-only status? What if there is an arcane flag buried in a hidden menu that only one person in support knows what it does? You need to be insane, brave or tenacious to refactor it.

In an ideal world, we would have centralized it from the get-go. We would have a centralized api. We would have locked up our business rules into one place. We would have discrete functions responsible for calculating our rights.

In the real world, permissions tend to grow with iterations of the product. We build our products with minimal architecture and focus on getting stuff done. As we build to the feature set or users want, the features and allowable actions change. Two years later, you look at your entire code base and see permissions everywhere.

A naive example

If an application’s permissions develop with each iteration, they grow like kudzu. It starts off with the same clean innocence of all new development projects. It starts off with the user object. That user object contains some logic like this:

  • User is an object containing all the information about a user. Transaction are the entities we care about in our hypothetical scenario. *
//Check to see if the user is an admin.
if (user.isAdmin) {
    delete(transaction)
}

Later, a business rule adds managers. They are a lot like administrators, but they don’t quite have the same level of access. For our scenario, they lack the ability to delete special transactions. Here’s a few naive examples to illustrate code we have all encountered at some point in our career.

//Now, admins and managers can edit because Product wants to empower managers
if (user.isAdmin || user.isManager) {
    edit(object);
}
//Managers still can't delete though.
if (user.isAdmin || ! user.isManager) {
    delete(object);
}

Getting more complicated

Assume success. Assume millions of users with lots of objects and lots of potential actions. Now, assume we need to change that logic. What if our manager cannot delete transactions in certain scenarios? What if they can in others? One possible solution is to garden our kudzu and make the changes in all those places … .

Maybe:

//Still can edit
if (user.isAdmin || user.isManager){
   edit(object);
}
//Only admin or a manager with the special privileges flag
if (user.isAdmin || ! (user.isManager && user.isSpecial) {
    delete(object);
}

Seeing the light

Brothers and sisters, there is a better way. We centralize and decouple. We make things a squinch more efficient. This magical panacea of all issues at scale works wonders here. We set up a RESTful api to return buckets of business objects ids. For each type of business object, we have buckets derived from the individual actions. In each of those buckets, we have the ids of the individual objects. A user can do an action to an business object if that id is in the bucket. Let your microservice do all the calculations and let your modules consume it. At the end of the day, a developer should only need to ask:

http://permissions.application.com/v1/:userID

The consumer gets back the barest possible information need to answer the question. We’re optimizing for speed here. IDs are small. Arrays are fast. Buckets provide the ability to lookup through hashtables the exact nodes.

We develop a two pass system for figuring out permissions. We can make it fast because we only care about small bits of information - what ids go in which bucket. We are free of the heavy lifting of returning all the user’s data. We can build out the permissions and only return the barest amounts of information. Since they are all ids, they have indexes in the database. Our looks up are fast. Our explained queries made the database administrators happy.

To calculate permissions, we retrieve all the header information for a user. We query our user tables and join their subscriptions and special settings. We build up object number one - the user permissions object.

{
    userID: 1
    canEditTransactions: true,
    canDeleteTranactions: true,
    canDeleteSpecialTransactions: true
}

{
    userID: 2
    canEditTransactions: true,
    canDeleteTranactions: true,
    canDeleteSpecialTransactions: false
}

Then, we get all the business objects a user cares about. Let SQL do what it does best - return relationships between ids and tables. We return the smallest amount of data. We return only the ids of the objects in their respective bucket. Now, we have the user permissions and their appropriate data. Reading through those returned record sets, we build a dictionary. This dictionary of permissions contains relevant user information and buckets of ids. It breaks down into the following actions:

transactions: {
    userID: 1,
    role: admin.
    //these are the transaction ids user 1 can delete
    canDelete: [1,2,3,4],
    //user 1 can edit these
    canEdit: [1,2,3,4],
    //user 1 can view these
    canView: [1,2,3,4],
    ...

}

Or for a different user:

transactions: {
    userID: 2,
    role: manager.
    //Hmm, one's missing.
    canDelete: [1,2,3],
    //Same here
    canEdit: [1,2,3],
    //But they can view them all.
    canView: [1,2,3,4],
    ...
}

From the data alone, we can see there is something special about object number 4. User 2 does not have the delete special transactions flag. Why can only administrators delete it? Administrators have the delete special transactions flag. What complicated business rules had fired to remove it from the bucket? The business rules governed by the permissions service.

There’s as many answers as there are permissions in your application. The key is that we only care about figuring that out within the permissions api.

The front end views and controllers rendering the templates should not have to care. They should only have to ask, you guessed it, “What can this user do to an object?” Based just on the user id and the object’s keys, we can now answer: they can do it if the id is in the appropriate bucket.

When rendering these transactions, a front end developer only needs to query the endpoint. They then control which widgets get rendered based on basic code. This code is completely independent of any changes to the permissions system.

//render a grid of transactions for a user
foreach(transaction in transactions) {

    //only show the ones we canView
    if ( transaction.id in permissions.canView ) {
   	 
   	 render(transaction);
   	 
   	 //But only show the big red button if they can delete it
   	 If ( transaction.id in permissions.canDelete ) {
   		 renderDeleteButton(transaction);
   	 }

    }
}

Same thing goes for checking assertions on the backend. When the server side controller receives a DELETE method on a given resource. It can use the exact same check.

/delete/:transactionID

if ( transactionID in permissions.canDelete ) {

    delete(transactionID);

} else {
    
    //deny the user
    throw(401);
}

But what about mutable permissions? What if permissions change based on parameters? Maybe the same user can edit transactions for one company? Maybe they can only read transactions for another? These business rules live in the permissions api. We answer those varied questions with query string parameters. We’re still getting the permissions for a given user for a given business object. We just parameterize our calls:

http://permissions.application.com/v1/:userID?companyID=1

Now, we GET the permissions.

Here’s where this gets cool: when the business rules change. A feature comes in and it tweaks the way a manager can interact with a transaction. In our naive example, every place where we checked for permissions, we would need to update our code. If that code has spread throughout our code base, well, that’s a lot more places to get it wrong. If we make the changes on the api, we need to make it only once. We have great impact with minimal code.

Of course, this is perilous. One wrong move in the game of permissions, and you can answer that question wrong for a whole lot of users all at once. That’s where part 2 comes in - asynchronous probing and comparing your results.

By
Brian Kotch is a full stack developer at InVision.

Like what you've been reading? Join us and help create the next generation of prototyping and collaboration tools for product design teams around the world. Check out our open positions.