Recently, I’ve had a chance to work with Pundit again. I wasn’t wowed with it previously, but I really took notice of it this time around.
What’s it for?
Why not CanCanCanCanCanCanCan?
In the past, when a Rails application needed authorization we reached in The Toolbox for CanCanCan.
Truthfully, in smaller, less intense applications it’s a matter of preference which gem to use.
However, as the applications grow more and more complex, the singular
ability.rb file used to define CanCanCan’s permissions can become more and more monolithic and potentially disorganized to hair-pulling proportions.
Policy classes to clear things up!
Let’s start with something straight out of the README:
class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? or not post.published? end end
With minimal effort, we can make reasonable, informed decisions about what’s going on here. The
Post object in question can be updated if the
user attempting the update has the
admin role, or the
post hasn’t been published yet.
Well, let’s build on the example above, shall we?
class PostsController < ApplicationController before_filter :verify_authorized def update @Post = Post.find(params[:id]) authorize @post @post.update(params[:post]) end end
verify_authorized is a helper method supplied by Pundit that, in this case, ensures
authorize is called in every controller action.
Behind the scenes, Pundit is making assumptions that
current_user will be available. (This lookup call is configurable, if, say, your application has a
current_mongoose… it’s possible… )
authorize call in our example is being passed
Post object.Pundit makes some assumptions here. It should check the
PostPolicy since it’s dealing with a
Post object, and that the permission matches the action name (
update?). We can, of course, override those assumptions.
class PostsController < ApplicationController before_filter :verify_authorized def update @Post = Post.find(params[:id]) authorize @post, :mongoose? @post.update(params[:post]) end end class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? or not post.published? end def mongoose? # I've really taken this mongoose thing too far... end end
Are you just going to rewrite the README?
Pundit did a well enough job on their own.
They made the
how? easy, you’re here for the
So why Pundit?
Simply put, it’s clean. It’s organized. It’s sexy.
I remember when I first started learning Ruby on Rails, the mantra was “skinny controller, fat model”. Also, “No logic in the views!”, “WTF is a
rake!?”, and “What’s up with that guy’s hair?”.
As the framework and the language have matured, there’s been a departure from the old “skinny controller, fat model” mantra; and as a programmer, I’ve come to embrace that.
Models should be kept lean and stay true to its ORM layer. Models define a table in Object Oriented space and its associations, delegates, callbacs and so forth.
Controllers should contain API specific code. What the application does.
We can keep our models and controllers nice and tight by taking advantage of ServiceObjects, Concerns, engines, Serializers - and a whole host of other tried and true approaches.
So why Pundit?
Policy objects allow you to organize your code in a way that just feels right. Your code is tighter, easier to read, easier to debug, more organized - where’s the downside here?
That’s why! :)