How we build granular access control and authorization system.
Authorizations are always been a problem. But we make it easy. This is how we came up with Permify, and how it works.
Authorization is usually something overlooked. It does not sound like something hard, isn’t it?
You can build a hacked solution.
Modeling the early use cases is pretty straightforward. (Admin or User, that’s all.)
For instance; adding roles table to our database, and linking to users’ database potentially solve your problem.
Yet, uses cases and features multiplies. By the time you need a new solution, shit got real! Your existing solutions fell short, users get more demanding, access control is nested in your business logic…
Every new feature, every new request becomes devastating for any developer. I heard no developer say “ Hurray, I’m building access control. ”, Neither heard saying “Hurray”.
But you get the point.
How did we know?
Because “Shit got real” for us several times. My co-founders and I have been building things together since high school. Almost a year ago, we started building an project management system for a client.
We start simple but things got complicated pretty fast, as the project grew clients need new access control features such as;
Only x can see that button or data
Only senior management can edit that data etc.
We have to update our access control several times, even after the production. Because their needs have changed.
Multiple times, as they grow…
And we have seen this scenario various times for startups, client projects, products that we have built, or been part of.
But, so what?
So, building a solution for authorizations is no easy task.
But decoupling your access control layer from your business logic might be a great first step.
Access control is something that usually has several shareholders, but just developers. That means you can’t just solve this problem with “if…else” statements that is embedded in your code.
Because one way or another; your Product, HR, and Design teams will need to interact with it. And if they’re interacting through the engineering team.
Oh boy…
You can build a decoupled system, which manages access control outside of your main code. This creates flexibility to improve your access control over time.
But building a future-proof external service for access control is a heavy load. It can’t be usually prioritized while having dozens of core product-related tickets.
Policy Engines
Another great solution is to use Policy engines. They usually cover most of the needs in terms of flexibility and complexity.
One of the most popular policy engines is OPA or Open Policy Agent. OPA is an open-source, and general source policy engine.
It basically decouples policy decisions from other responsibilities of an application, in other terms business logic.
You might ask what’s the catch?
Yeah, OPA is damn hard to learn. It could cover more than what you need. But with a longer learning curve.
It might take weeks, or even months to implement OPA. Which means less time or resources for your core product.
That’s why we built Permify.
We wanted to build access control as fast as possible without dealing with the complexity, and learning curve.
So, We have spent weeks learning how to build this thing instead of building just simple access control. So that you can set up complex and flexible authorizations in under 30 mins.
Why because we’re developers.
What is Permify?
Permify is a plug-&-play authorization API, that makes you build and deploy access control solutions in 30mins.
You can easily create complex and flexible RBAC and ABAC solutions without dealing with a heavy learning curve.
How It Works?
We have defined 3 concepts for the authorizations process. Respectively; Policy, Option, and Rule.
Policy
Policies are access control layers that consist of rules and options. You can define who, and in which circumstances can access certain resources, actions, or data points.
To do that, you can create different combinations of;
User roles
User Attributes
Resource attributes
And make comparisons between them. Policies turn you with an access check response by using these rule sets, options, and attributes.
Go Client:
response, err := client.Policy.Create(policy.Create{ | |
Name: "task edit policy", | |
OptionNames: []string{"senior manager", "resource owner"}, | |
}) |
Option
Option is a cluster that works as an “and” operator between Rules. And you can create “or” relations between different rulesets by using Options.
For instance, as you can see in “Image 1”; Senior Manager and Resource Owner are two separate options nested in a policy.
According to that policy, users can edit tasks either if they’re senior manager “or” resource owner.
Go Client:
response, err := client.Option.Create(option.Create{ | |
Name: "senior manager", | |
RuleNames: []string{"is senior", "is manager"}, | |
}) |
Go Client:
response, err := client.Rule.Create(rule.Create{ | |
Name: "is resource owner", | |
Conditions: []string{"user.id == resource.attributes.owner_id"}, | |
}) |
Rule
Rules are functions in which you can compare user roles, user attributes, and resource attributes.
As seen in the “Image 1”; “is senior”, “is manager” and “is resource owner” are examples of rules.
Sample Rules
the use senior? (ABAC)
user.attributes.tenure > 8
Go Client:
response, err := client.Rule.Create(rule.Create{ | |
Name: "is senior", | |
Conditions: []string{"user.attributes.tenure > 8"}, | |
}) |
Is the user manager? (RBAC)
"manager" in user.roles
Go Client:
response, err := client.Rule.Create(rule.Create{ | |
Name: "is manager", | |
Conditions: []string{"\"manager\" in user.roles"}, | |
}) |
Is the user the owner of the resource? (ABAC)
user.id == resource.attributes.owner_id
GO client:
response, err := client.Rule.Create(rule.Create{ | |
Name: "is resource owner", | |
Conditions: []string{"user.id == resource.attributes.owner_id"}, | |
}) |
Result Policy Object
{ | |
"name": "task edit policy", | |
"guard_name": "task-edit-policy", | |
"description": "", | |
"options": [ | |
{ | |
"name": "senior manager", | |
"guard_name": "senior-manager", | |
"rules": [ | |
{ | |
"name": "is senior", | |
"guard_name": "is-senior", | |
"description": "", | |
"conditions": [ | |
"user.attributes.tenure > 8" | |
] | |
}, | |
{ | |
"name": "is manager", | |
"guard_name": "is-manager", | |
"description": "", | |
"conditions": [ | |
"\"manager\" in user.roles" | |
] | |
} | |
] | |
}, | |
{ | |
"name": "resource owner", | |
"guard_name": "resource owner", | |
"rules": [ | |
{ | |
"name": "is resource owner", | |
"guard_name": "is-resource-owner", | |
"description": "", | |
"conditions": [ | |
"user.id == resource.attributes.owner_id" | |
] | |
} | |
] | |
} | |
] | |
} |
Check Authorization
Check Authorization is the function that you can use both on the server and client-side.\
Server Side Authorization
You can find out if the user is authorized to perform the action by sending the user ID, policy name, resource (optional) to Permify.
Go Client:
Client-Side Authorization
Using client-side authorization can help you protect your app even further while also making it easier to control access on various UI components, Layers, Pages, etc.
isAuthorized(policyName)
isAuthorized is a helper function that returns a Promise which turns true if the user is authorized for action with the given parameters, if not false.
PermifyComponent
PermifyComponent is a wrapper that controls components or UI Layers that should only be accessible to authorized users.
We have just separated our access control from the business logic. But the main challenge of decoupling access control occurs after this.
Because decisions are consists of both authorization and application inputs. (e.g. which role the user has and who created the role.) It is hard to sync your users and resources access control attributes in an external service.
For that reason, we have created following API calls for users and resources;
user create API call:
response, err := api.User.Create(user.Create{ | |
ID: "1", | |
Name: "tolga", | |
GroupID: "1", | |
RoleNames: []string{"manager"}, | |
Attributes: map[string]interface{}{ | |
"tenure": 7, | |
}, | |
}) |
resource create API call:
response, err := client.Resource.Create(resource.Create{ | |
ID: "1", | |
Type: "task", | |
GroupID: "1", | |
Attributes: map[string]interface{}{ | |
"owner_id": "1", | |
}, | |
}) |
While creating resources and users, you can add the attributes you want. These attributes can be later used in the rules and policies to make access control decisions.
Ending Notes
In short; even it seems easy to build and manage simple authorizations, it gets hard and unmanageable over time with different customer requests.
On the other hand, it’s not feasible to build a future-proof system from scratch. Because it’s not usually a core product feature.
If you feel the same as we did, Permify helps you build this complex and future-proof system in 30mins. So you don’t have to worry about any development effort.
For more information, you can check out our docs from this link.
And if you want to use Permify you can signup from this link.
If you have further questions, feel free to reach out at tolga@permify.co