I’ve been working with Ruby on Rails a lot recently. It’s a great platform for creating both old-fashioned web applications as well as services for RIAs. I especially like the fact that Rails encourages you to think about the right way to do something, before writing any code. For all you know, that complex thing you wanted to add could involve nothing more than one shell command and a tweaked line of Ruby code.
There was one seemingly ubiquitous thing that I had to think about for long time however, so I’m posing it here for posterity: How do you pass arguments to an ActionController method from a filter, while also using filter conditions?
It seems like such a common problem. You buy the Agile Web Development with Rails book, like everybody else, read half the first chapter, then start writing your own application. At some point you need to add user authentication to your application. If you were working with any other language/framework, this would have taken waaayyyy longer than it did to copy-and-paste the authentication example in chapter 11. Here’s the example I’m referring to:
my_app/app/controllers/application.rb
class ApplicationController < ActionController::Base
session :session_key = "_my_session_id"
private
def authorize
unless User.find_by_id(session[:user_id])
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end
This method gets called automatically by specifying so on a filter in the individual controller, e.g.
my_app/app/controllers/item_controller.rb
class ItemController < ApplicationController
before_filter :authorize, :except => [:show, :search]
# ... various methods ...
end
Then, several features later, you realize that you need to have different levels of user (administrator, moderator, gnat, etc.). The authorize method now needs to take an argument of what type of user is required. You would have to specify this when you write the filter (in item_controller.rb), but you need to retain the filter conditions (:except). This took me a long time to find. The reference page on filters didn’t help.
Filtes can pass arguments to the methods they call, but the syntax is not obvious:
- The authorize method you call must be public.
- Your filter conditions have to be the first argument to the filter.
- Instead of referencing the authorize method, you will need to call it, but you must do so from a code block, and that must be the last argument to the filter. Note the do … end in the example below.
- Â You can pass as many arguments as you like to the method inside this code block, since you’re actually calling it here.
- To call the method, you need to define a block variable inside your code block (it can be named anything you like). This will be a reference to the controller. Note the two pipes in the example below.
So here’s the controller class with filter:
my_app/app/controllers/item_controller.rb
class ItemController < ApplicationController
before_filter :except => [:show, :search] do |controller| controller.authorize({"required_user_level" => "administrator"})
end
# ... various methods ...
end
Don’t forget to add an argument to your method declaration in the ActionController, and to make it public:
my_app/app/controllers/application.rb
class ApplicationController < ActionController::Base
session :session_key = "_my_session_id"
def authorize(vars)
puts "Required User Level is " + vars["required_user_level"]
unless User.find_by_id(session[:user_id])
flash[:notice] = "Please log in"
redirect_to(:controller => "login", :action => "login")
end
end
Very timely. Does seem to move you forward. On the other hand, the public authenticate method is now accessible as an action on the controller. So kinda a trade-off.
Even the restful auth guys don’t have a good way around this.
Couple of clarifications:
“Very timely” – i.e. I had just run into this problem myself.
“public authenticate method” – I meant “authorize”, like in your example.
Wouldn’t protected also work? In your example “controller” is an ItemController and a subclass of ApplicationController, where you’ve defined your authorize method. So I would expect that the child could call a protected method on instance of the parent class.
I’m going to try that anyway … no better way to learn to walk than by falling down.
Hey Doug,
I tried making the authorize method both protected and private, but that results in a NoMethodError.
I’m not sure why exactly – it didn’t make sense to me either. But in my install it definitely only works when the authorize method is public.
-Antun
Almost perfect! I’ll definitely be hacking around with this shortly to see if I can find a way to prevent exposing my internal methods
I found following 2 ways how to avoid exposing your filter method and still pass arguments to your filter method:
method 1:
before_filter do |c|
c.class.module_eval do
private
def custom_filter
authorize(args)
end
end
end
before_filter :custom_filter
method 2:
before_filter do |c|
c.send(:authorize, args)
end
@ jano, the second way works perfectly!
Thanks…