Authorizing your Rails app with Authority
Imagine you’re writing a Rails app to organize conferences. As soon as you know what the app can do, you have to start deciding who can do what. Who is allowed to:
- Decide who will speak at the conference?
- Edit the presenter schedule?
- Upload presentation slides?
- Comment on those slides?
- Create playlists of music?
- Make a personal schedule of which talks to see?
All these questions are about authorization: “what is this user authorized to do?” Obviously, it’s important to get this logic right: it needs to be correct, and it needs to be consistent. And it would be nice if that logic was grouped together, rather than scattered all over your app.
I’ve just released a gem to solve this problem cleanly. It’s called Authority (see Github and Rubygems).
Protecting Your Models
Before I show you how Authority works, let’s talk about some of the general ideas.
Authority’s notion of permissions in your Rails app is focused on your models; in the example above, these would be presenters, comments, playlists, etc. In some cases, the question is very simple: a conference attendee cannot make any changes to any presenter, period. You can think of that as a “class-level” rule: the Presenter
model is read-only for anyone who isn’t a conference organizer. If an attendee tries to visit the page for editing a presenter’s bio, we don’t have to ask any questions about this particular presenter to know that the action is not allowed.
In other cases, the question is more nuanced: attendees can edit their own personal schedule, but not anyone else’s. You can think of that as an “instance-level” rule: to know whether a conference attendee can edit a schedule, you have to look at that schedule instance and see who it belongs to.
Using Authority, you’d use class methods, like def self.updatable_by?(user)
to set class-level rules, and instance methods, like def deletable_by?(user)
, to set instance-level rules.
But where should those methods be written?
Keeping Your Permissions DRY
Obviously, different models have different rules, so you might think that authorization methods should go on the models themselves.
But it’s likely that some of your models share rules: anyone who can edit a Presenter
can also edit the PresenterSchedule
. If that’s true, it would be nice to keep that DRY: let the Presenter
model and the PresenterSchedule
model use the same authorization logic.
Authority accomplishes this by having the model delegate any questions of authorization to a specified Authorizer class. Models with the same rules can point to the same Authorizer.
An Example
To take a simple example from the gem’s README, suppose you two have categories of resources in your app: some for regular users and some for administrators. Using Authority, you’d have two authorizer classes. You might call them BasicAuthorizer
and AdminAuthorizer
.
You could group your models like this:
[gist id=’d38d297e3ed9a3411c3a’]
In this example, the Comment
model’s authorization rules come from BasicAuthorizer
, but Article
and Edition
get theirs from AdminAuthorizer
. You’d call self.authorizer_name =
on each model to set this up.
The AdminAuthorizer
might have a method like this:
[gist id=’0f9d6cb90948117b47fe’]
Anytime a user tries to create an Article
or an Edition
, this method will be called. If the user isn’t an admin, it will return false and the action will be denied.
Any method that isn’t defined on an authorizer will be inherited from Authority::Authorizer
, which will consult a configurable default_strategy
proc. The built-in default strategy simply returns false. This is a whitelisting approach: any action you don’t explicitly allow will be forbidden. But you can supply your own default strategy to do something more nuanced.
So the full lookup chain looks like this:
[gist id=’e577d3ef3b988aa2a1a0′]
Standard Ruby Classes And Methods
The nice thing about this structure is that it’s just regular object-oriented programming. Your authorizers are just classes, so you can modify them any way you like: include modules, change the parent class, metaprogram, etc.
In addition, the authorizer’s methods are just plain Ruby methods: there’s no new syntax to learn. Authority makes no assumptions about what logic you’ll need. You can consult a database or a file or a web service to make your decisions; if you use a database, you can use whatever ORM you like. All the logic is up to you; Authority just helps you keep it organized.
Syntactic Sugar for Users
So far we’ve seen our authorization methods in the passive voice: is this resource creatable by this user? But it’s nice to be able to say the same thing in an active voice: can this user create this resource?
Like several other popular authorization gems, Authority gives you this syntactic sugar. current_user.can_edit?(@article)
is simply a pass-through to @article.editable_by?(current_user)
, which in turn would ask the AdminAuthorizer
.
Since can_edit?
and similar methods are defined on the user object, you can use them anywhere that object is available. A common case would be to show links only to users who should see them:
[gist id=’322a41a42087a7c091d1′]
A Little Magic for Controllers
If you’re using the method shown above to hide links from unauthorized users, most people won’t try anything they shouldn’t be doing. But what if someone manually types the URL to edit a resource that’s forbidden for them?
At that point, your controller must intervene. Authority gives you a couple of methods for this: authorize_actions_for(ModelName)
, which checks class-level permissions and will stop the controller method from ever running, and authorize_action_for(@model_instance)
, which checks instance-level permissions from inside a controller method.
In either case, forbidden actions are handled by a controller method that you specify; the default one logs the user’s action and displays a warning.
Check it out
You can get more detail on everything discussed here by looking at the README on Github. I’d also encourage you to read the source code; Authority isn’t a large gem, and the source is well-commented. You may learn something, and of course, reading the code is the first step towards contributing.
Happy hacking!