Directory-based authentication in Rails
I think it’s safe to assume any Rails developer is familiar with something like before_filter :login_required at the top of any controller that, well, requires a login. One can’t complain about how tremendously easy that is. But I recently did something terribly stupid that has provoked me to take it a step further.
Don’t be Stupid
As you may know, I cleaned up my site for 2008. Well, in doing so I stripped out the inline admin interface in favor of a more organized (and RESTful) approach by adding an admin namespace. I love namespaces. But anyway, with the addition of these new namespace’d controllers, I forgot to add the one-liner: before_filter :login_required. And to top it off, I just realized this 2 nights ago. So all of my admin functionality was open to the public since December 31. Way. To. Go.
Putting a Directory on Lock-down
Granted that was completely my fault, I wanted to remove that worry, as well as the need for the login_required method to be called per controller. With namespaces, you get a nested directory within your controllers directory. In my case, I have: app/controllers/admin. That’s where all of my (you guessed it) admin functionality goes. It’d be much easier for me to just restrict access to that directory, so that any controller that finds its way in there automatically requires a login. Apparently, I just can’t trust myself anymore.
Now, this is the first run-through and I haven’t had the “go back the next day and clean it up” moment yet, but here’s essentially what I’m doing…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # controllers/application.rb class ApplicationController < ActionController::Base include Authentication before_filter :should_authenticate? # ... end # lib/authentication.rb module Authentication RESTRICTED_NAMESPACES = %w(admin) protected def authenticate # choose your flavor... # (I'm using HTTP basic authentication) end def should_authenticate? RESTRICTED_NAMESPACES.each do |directory| if controller_path.match(/(^.+)\//) authenticate if directory.downcase == $1.downcase #&&controller_exists?($1) end end end #def controller_exists?(dir) # controllers = [] # Dir.glob("#{RAILS_ROOT}/app/controllers/#{dir}/*_controller.rb").each do |f| # controllers << $1.downcase if f.match(/^.+\/(.+)_controller.rb$/) # end # controllers.include?(controller_name.downcase) #end end |
Note: I’m not sure if adding a :path_prefix in the route declarations will screw with the controller_path (I’m pretty sure it doesn’t), but that may be something to keep in mind.
Conclusion
I’m currently using this on my new portfolio, and so far it seems to work very well. It’s nice to just generate an admin::whatever controller and know that it’s automatically protected. I’ll probably modify this a bit for a plugin and start using it in other applications, so feel free to chime in if you see any obvious flaws that I may have missed.

Simon Friday, 25 Jan, 2008 Posted at 01:58AM
You can also make all your admin controllers inherit from Admin::AdminController or something, then put the before_filter in the Admin::AdminController. (That’s what I do—I usually except out the login action, of course.)
Ryan Friday, 25 Jan, 2008 Posted at 04:36AM
Yeah, that was actually my first run at it, but I still had to ensure that I changed the inheritance from
ApplicationControllertoAdmin::AdminController, which is pretty much back to the issue of putting thebefore_filterin every controller (as that’s the only reason I would inherit from theAdmin::AdminController). But I know what you’re saying.And of course, it’s hard to fathom that I’m complaining about adding one line to a controller to ensure authentication. That’s so clean and simple, there’s almost no reason to try and do better. But you’ll have that, I suppose. Thanks for the suggestion.
Chris Friday, 25 Jan, 2008 Posted at 12:54PM
Geez, Ryan, quit being so lazy about authentication ;-) I personally prefer the idea of AuthenticatedController as a superclass, but it’s true that the generator scripts won’t set that for you.
Your solution takes care of that, which is good – and I can see why you went that route since you already had the controllers created. I would suggest, however, RESTRICTED_NAMESPACES instead of directories. Also, I’m curious why you check that the controller exists – that may be unnecessary overhead.
Maybe something like this:
Ryan Friday, 25 Jan, 2008 Posted at 04:22PM
You know, I don’t know why I do a lot of things, and why I’m checking if the controller exists is no exception. I guess I was looking for reinforcement. It’s one thing to do be overcautious in a test, but you’re right, it’s unnecessary overhead and has been booted. And yes, restricted_namespaces is much better than restricted_directories ;-)
The reason I’m using a regex instead of something like
starts_with?is becausecontroller_pathreturns the entire system path (/Users/.../admin/some_controller.rb), so I had to get the folder before the last slash.And now (I knew this would happen) I’m having mixed feelings about the whole thing, and may go back to the superclass.
Sometimes I try to take a different approach, instead of always following the same path, even if I don’t end up using it in the end. In a way I sort of feel like I can form better opinions about the things I do use.
Also, it’s kind of tough not working with any other Ruby/Rails developer to share opinions/methods with. I mean, I guess I read my fair share of articles, and post the occasional “authenticated-directory” post, but it’s not the same.
Anyway, I appreciate the feedback.
Chris Friday, 25 Jan, 2008 Posted at 07:10PM
controller_pathreturns the actual file path? That doesn’t seem right.Anyway, taking a different approach is great – you’ll learn more that way, trying new solutions.
And I know what you mean about not working with other Ruby devs. – sometimes you just need to talk code and implementation and bounce ideas around.
Who knows, maybe you’ll work with me on slate ;-)
Ryan Saturday, 26 Jan, 2008 Posted at 06:55AM
It’s weird, I looked that up, too, but when doing a
putsto the terminal, or playing in the console, I was getting the entire system path. I’ll mess around with that some more, though, now that you’ve confirmed how it should behave, as well.