March 08, 2024

Authorization Help

When it comes to authentication and authorization in a rails app, there's usually two things that quickly come to mind.

1. Authentication - This is if a user is logged in and we know their identity. That's really the crux of it: you are who we say you are and we can identity you.

2. Authorization - What data do you have permission to access. In a multi tenanted app, this means you should only access records for your current logged in tenant (there are sometimes valid use cases for accessing records across tenants). But authorization also extends to accessing records within the tenant. A sales manager may need access to all the sales records for the territories she manages, but a sales person, might only need access to their territory. I usually favor multi level roles for users (or more specifically their memberships) that offer flexible permissions for different areas (Ex: a user may be the sales manager for multiple territories, an observer/read only for another territory, and also need access to payments received.)

Validation - Sometimes it might make sense to make use of ActiveRecord validations to "authorize" or validate data saved. Let's say for example a sales person has permission to create and edit an invoice, but we don't want to allow them to back date the invoice to a previous month (so they meet their sales goals and get that sweet bonus).  

The problem - Two Examples

But an accounting controller might need to back date something (maybe it was typo'ed into 3024). This starts to feel wrong. We now need to know the user and their role in an ActiveRecord model? The model doesn't usually know who the changing user is. 

Consider another example. We have a Quickbooks integration in TRACT. When users generate invoices in our system, they can send them to Quickbooks. Once they're sent, we don't want to allow users to edit the invoices in TRACT, they are "locked". An invoice has many line items and so those would need to be locked too. Most line items and invoices are generated from tickets or work orders, which can create new invoices but also be added to an existing invoice. So we don't want any additional tickets added to locked invoices. 

We could provide some authorization around this business logic. But usually authorization either allows or disallows an action (read, update, delete, etc). Usually when a user tries to access a record or update a record of which they are not authorized, they'll get a not authorized error. In our two examples, we want to allow the user the access the record and maybe even modify some attributes, but not others or maybe even modify the attribute, but within a certain range. 

I love the Pundit gem and it provides a way to whitelist attributes under certain business logic. But, it's important to tell the user why that can't change a particular attribute or why a certain value isn't permitted. 

The Goal

What's the best approach (pattern, tool, etc) for this? The goal would a way to define rules for an object attribute based on a user's roles. If the user submitted a modification that broke the rule, it would still save other attributes that were valid, but prevent the attribute that broke the rule and provide the user a message (similar to a validation error) of why the change wasn't permitted and offer them an opportunity to change it. 

I figure I could write some custom logic in in my policy but I'm mostly curious if there's already a pattern or gem out there that already addresses this. I don't recall reading or hearing about this before.