3 acts as current user container
acts_as_current_user_container is a mixin for ActionController::Base classes.
3.1 Gotcha
Note that you have to call current_user= (the setter) with self.current_user=. Otherwise, the local variable current_user will be set. We we will write self.current_user below to stress this. Note that with self.current_user we mean the object method not a class method!
3.2 Motivation
All Rails applications featuring access control must store the currently logged in user in the session. Sessions are only available from your controllers so you will either have a lot of session[:current_user_id] calls in your code or write a method to access the current user via this method.
This method is very simple and thus it can perfectly be refactored into a mixin for your controllers. We have done just that and wrote acts_as_current_user_container.
3.3 First Glance
Let have a quick look on how it works before diving into detail (you could also call this section “for the impatient”). If you want to access the current user in multiple locations then you will want to put the call to acts_as_current_user_container into your ApplicationController:
class ApplicationController < ActionController::Base
acts_as_current_user_container
end
Afterwards, you can get access to the current user in your controllers by the methods current_user and self.current_user. For example:
class SecretController < ActionController::Base
before_filter :protect_me
protected
def protect_me
if current_user.nil? then
session[:errors] = 'Keep out!'
redirect_to login_url()
return false
end
true
end
end
As you would guess by the example above, the current_user returns nil if no user has previously been set. This makes for some checks whether the result is nil or is not nil. You can circumvent this problem by passing in a symbol specifying a class (or the class itself):
class AnonymousUser
acts_as_anonymous_user
end
class ApplicationController < ActionController::Base
acts_as_current_user_container :anonymous_user => :anonymous_user
end
Then, current_user creates an instance of AnonymousUser and returns it if no user has been set explicitely. See the documentation on acts_as_anonymous_user for more information about this mixin.
3.4 Requirements
The acts_as_current_user_container mixin works with any ActionController::Base class (in fact the only dependency is the existence of a session method returning a Hash).
You must not use the session variable :aacuc_data, i.e. session[:aacuc_data] should not be accessed from your code.
3.5 Reference
When you use the mixin in your ActionController::Base class then the two methods current_user and self.current_user are added to this controller class. You can set an object that represents the current user by using the second helper method. The first one returns the object earlier set.
If you call current_user without calling self.current_user before in the current session then the result depends on the parameters you passed to the acts_as_current_user_container method. At the moment, this method accepts but one parameter - :anonymous_user. The following are valid values for :anonymous_user
- nil (the default) - current_user returns nil.
- a symbol - The symbol will be converted into a class name and a new instance of this class will be returned and automatically set with current_user.
- a class - This class will be used and the behaviour is the same as when a symbol is passed in.
Examples:
class ApplicationController < ActionController::Base
acts_as_current_user_container :anonymous_user => nil
end
class ApplicationController < ActionController::Base
acts_as_current_user_container :anonymous_user => :anonymous_user
# Results in AnonymousUser.new to be returned by current_user
# unless something has explicitely set within this session.
end
class ApplicationController < ActionController::Base
acts_as_current_user_container :anonymous_user => AnonymousUser
# Results in AnonymousUser.new to be returned by current_user
# unless something has explicitely set within this session.
end
If the value of current_user is an ActiveRecord::Base object then the id and the class will be stored in the session. If the value of current_user implements the informal PersistableInSession interface (see below) then the methods of the interface will be used to store the class and the data necessary for unpersisting in the session.
In all other cases only the class will be stored in the session and {previous current_user}.class.new will be returned on the next call to current_user. No state is preserved in this case.
3.6 The Informal Interface PersistableInSession
If an object that is to be written into the session data via current_user is not an ActiveRecord::Base object and needs to preserve its state over requests in the session then the object and its class must provide the following methods. We refer to them as the “informal PersistableInSession interface”. In a language like Java they would be collected in an interface but since Ruby does not have (and only arguably needs) interfaces it is only informal:
- persist - This object must respond to this method and return an object that can be easily marshalled. We recommend you to return a String, Fixnum, Float or Array or Hash containing these classes.
- class.unpersist(param) - The object’s class must respond to this method. It is given the data earlier returned by persist and must restore the object by this state.
The information to be stored in the session should be as few as possible: For example, a numeric user id, a login name or a pair of the login name and the Windows Domain.
If your user data is comes from an LDAP server then your User class could look like the following:
File app/model/user.rb:
class User
attr_accessor :login
def persist
{ :login => self.login }
end
def self.unpersist(hash)
user = User.new
user.login = hash[:login]
return user
end
end
3.7 Security Implications
You should be careful only to persist the absolutely minimal information that is required to identify a user into the session. The name of the class of the object to unpersists and the object id for ActiveRecord objects is a good example of this.
If you store a password hash then this information will be written into the session file - if you use file based sessons - in a temporary directory.
Storing information like permissions or information about related ActiveRecord::Base objects could lead to incoherent data: The user object you store in the session has permissions and roles that have been revoked from the given user from the database.
Other problem scenarious could be inexplicable crashes because your user object tries to reference no longer existing articles.
3.8 Testing Gotcha
Note that calling @controller.current_user will not work in your tests before you called either post or get. You can set the current user in @controller in tests by using the Test Helpers described below.
3.9 Test Helpers
ActiveRbac comes with a helper for your controller tests. You can use it by including active_rbac/test_helper in your tests.
The helper adds the method set_current_user to Test::Unit::Test. This method works exactly as the current_user= method in your acts_as_current_user controllers.
3.9.1 Example
File app/controllers/application.rb:
class ApplicationController < ActionController::Base
acts_as_current_user_container :anonymous_user => AnonymousUser
end
File app/controller/test_controller.rb:
class TestController < ApplicationController
def action_that_requires_login
render :text => current_user.inspect
end
end
File app/test/functional/test_helper.rb:
# ...
require 'active_rbac/test_helper'
# ...
File app/test/functional/test_controller_test.rb:
# ...
def test_action_that_requires_login
set_current_user(users(:first))
get :action_that_requires_login
end
# ...