I’ve been planning to try something new with regard to authorization in my current side-project and I’m tossing up between a few different options. I’m curious to know what you guys think about the following?
1. Moving authorization logic to constraints in your router?
I’m sort of liking this idea as it removes logic from your controller altogether and authorization + redirects are happening when the request comes in. I’m a bit unsure about what’s going to happen when the constraints begin to require a bit more logic and if my routes file is going to become littered with redirects containing flash notices.
I noticed Clearance has an option to use Constraints so I was wondering if the team at Thoughtbot are using them as a means for Authorization?
2. Create ‘Policy’ objects (perhaps using a gem such as Pundit)?
A clean and simple solution. The gem does make a few decisions for you and applies syntactic sugar, so I guess the question is why not just roll your own or something similar?
3. Use CanCanCan (assuming you want a more up to date CanCan gem)?
Slightly older approach with regard to Authorization. Is it still good?
This blog post is obviously just one example of rolling your own Authorization but it’s a good example to highlight my point. I was a bit unsure about his approach when I first looked at it, but the more times I read his post, the more I begin to like what he’s done with the Authorizer::Base class. His tests are also pretty tight too.
Did I miss any other solutions? What do you guys think?
Number 1 seems like a good idea in principle, I’d like to see an implementation of that.
I’ve never used Pundit (I usually use cancan), but it seems to solve a problem that cancan has, that is when your ability file gets to big and makes more request to the database than what would be nice. There are ways to fix this, such as having multiple ability files, which I believe is kind of the direction of pundit. Will have to give it a try.
Cancan does have a very nice feature that is check_authorization, that for each controller action checks if you have authorized or explicitly skipped the authorization. It’s great if you keep forgetting to authorize actions like me.
We can obviously roll your own, but if you do, turn it into a gem asap, if you intend to use the same approach in multiple projects.
I have done routes bakes into auth, and it is kind of like the problem with observers. You can get strange errors that are not always intuitive. Very similar to using default_scopes and then hiding half your records.
So for these reasons I stay away from auth controlling my routes.
After Pundit, I would probably roll my own. But I am famous for wasting time on rebuilding tools that already exist because I don’t want an extra dependency and half the features provided in the gem
As far as anything that looks like Authorizer::Base - I hate these, they violate the liskov substitution principle. All your base… liskov wiki entry
I am not a thoughtbot employee, just a rubyist with a lot of time in rails
I’ve been using the rolify gem and checking roles to only provide certain routes in the routes.rb file. I’m planning to use pundit as well for object level authorization. However, I didn’t see any need for involving pundit at the routes level. Did I miss anything? Anybody else using this strategy?
This is what it looks like. I’ve got all my admin functions under =/admin=. The redirect at the end is necessary to avoid getting a route not found error.
(Code critique invited!)
MyApp::Application.routes.draw do
scope :path => '/admin', :as => :admin do
get '', :to => 'pages#admin'
authenticate :user, lambda { |u| u.is_admin? } do
mount RailsAdmin::Engine, at: 'data', as: 'rails_admin'
mount Sidekiq::Web, at: 'sidekiq', as: 'sidekiq_admin'
end
end
# Redirect to root if no matches
if ['production', 'test', 'local_production'].index(Rails.env)
get '*path' => redirect('/')
end
end
I found that Policy objects are not hard to write and test against. Unlike authentication, where I think you would be very ill-advised to try it yourself, authorization has fewer gotchas, and it’s easier to look at your tests and make sure you’re checking the right things. You can also build compound policies by decorating policies with other policies if you design your policies in the right way.
Haven’t looked at Pundit, but I would advise trying to build some very simple objects on your own, find the pain points, and then see if Pundit fixes those.
I’ve used cancan extensively and the #1 thing I liked about it was the feature to by default require that authorization is considered for all controller actions. Pundit offers the same functionality, so I will likely try it. Without this, I think it would be too easy to forget authorization on some controller methods. I.e., I’d prefer to explicitly state which methods are not using authorization than using it.
@zamith I agree constraints intrigue me but I have a feeling the end result won’t be pretty. The large ability file using CanCan is a concern and it’s sort of the reason I started the thread. If I come up with a good solution, then yes I’ll put it in a gem.
@Dreamr thanks for pointing out the Liskov Substitution violation in this example. I did a quick refresher on the principle and can see what you’re saying here. Perhaps I was getting a bit sentimental about Rails and appreciated the ‘throwback’ to using Base pattern
@Justin_Gordon I just had a quick breeze through the Rolify documentation. I have a feeling if I used that I would just end up creating a Policy object anyway? I’m not sure you’d use Pundit at the routes level either, as far as I understand, you’d just extract authorization logic into Policy objects and call them in the controller.
@geoffharcourt Yes I think I’m heading in that direction - rolling my own solution may be the way to go and may be good practice too.
Thanks for the tips. I think my conclusion so far is that i) try out Pundit (I don’t think it’ll take long to give it a test drive) and if that doesn’t work out ii) roll my own authorization (perhaps throwing it into a /lib directory without inheriting from a Base class).