24 Jan, 2008

Published at 04:05PM

Tagged with authentication, programming, rails, and tips

This post has 6 comments

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.

Comments

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 ApplicationController to Admin::AdminController, which is pretty much back to the issue of putting the before_filter in every controller (as that’s the only reason I would inherit from the Admin::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:

1
2
3
4
5
def should_authenticate?
  authenticate if RESTRICTED_DIRECTORIES.detect { |e|
    controller_path.starts_with?(e + '/')
  } 
end

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 because controller_path returns 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_path returns 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 puts to 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.

Do you have something to say about this post?
Retype the image to the right Spam Hint: Are You Human? Textile Formatting Tips

or

Ryan Heath | Site Management A Ruby on Rails production.

This site is a Formed Function. Formed Function LLC | @formedfunction | Get in Touch