There was a problem with this method that I have been able to fix so I
am posting here to help others.
Putting in
acts_as_authentic do |c|
c.validations_scope = :company_id
end
does indeed help with emails, but the problem is that the User class
select still doesn't get the company_id added to it. And a foible of
authlogic is that in the user_sessions controller where all the action
of logging in really takes place, the before_filters mean that the
User model is instantiated:
In UserSessionsController:
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => :destroy
]and in ApplicationController:
private
def current_user_session
return @current_user_session if defined?
(@current_user_session)
@current_user_session = UserSession.find
end
def current_user
return @current_user if defined?(@current_user)
@current_user = current_user_session &&
current_user_session.user
end
def require_user
unless current_user
store_location
flash[:notice] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end
def require_no_user
if current_user
store_location
flash[:notice] = "You must be logged out to access this page"
redirect_to :controller => :mainMenu
return false
end
end
def store_location
session[:return_to] = request.request_uri
end
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
# Authlogic stuff ends here
the line with:
@current_user_session = UserSession.find
is the guilty party here. It means that your UserSessions controller
before_filter is going to try to find a UserSession in order to
determine if there is a valid user session in play already. And
UserSession instantiates an instance of the User model
But now the problem is, if you are limiting your User model to a
particular company ID, it needs to be passed through from UserSession.
But UserSession in authlogic is written to only pass through username
and password, not company ID. Even by adding:
attr_accessor :company_id
to the UserSession model it still doesn't pass that through to the
User model when it instantiates it.
And bear in mind that the before_filter problem (see above) means that
even before you try to create a UserSession for real in your user
session controller, the before filter check has already tried to
instantiate an instance of User model when it checks to see that you
are not already logged in!
So because of the way the UserSession -> User chain works in
authlogic, getting the company ID into the User model won't work
through the usual authlogic mechanisms.
So the solution that I found was to create a global called $companyID.
This gets set in the user session controller:
def create
$companyID=params[:user_session]["company_id"]
Okay, so now you have a company ID, but how do you get the User model
to restrict itself to that company? The select using
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.validations_scope = :company_id
end
looks something like this:
SELECT * FROM "usertable" WHERE (LOWER("usertable".username) =
'myLoginID') LIMIT 1
I.e. it will bring back 1 row from the database only. So you cannot
use the usual exclude filtering tricks in the model to strip out the
rows that are not for this company. Hmmm... and authlogic is clearly
clobbering any attempt to add companyid to the where clause.
So what we need to do is go below the authlogic layer. How about using
scopes? Okay, if you try to create a named scope, you still can't get
authlogic to use it when it's instantiating the User class. Not
without modifying the code, that is.
Now Rails has the wonderful default_scope capability and at first
thought, if you could use that!
default_scope :conditions => ["company_id = ?", $companyID]
But... there is a problem. Yes, the first time through it nicely
appends the company_id = ABC to your where clause. But then try a
second time, try putting in company ID XYZ and the where clause STILL
HAS ABC appended to it! Unfortunately it appears that the
default_scope is cached and if you pass a variable to it, it doesn't
re-read the variable every time you instantiate a model, it uses the
cached value that it worked out the first time!
So the caching of default_scope breaks what would otherwise have been
a very simple, very elegant solution.
So we have no choice but to subclass the model's find method:
class User < ActiveRecord::Base
def self.find(*args)
with_scope( :find => {:conditions => ["company_id = ?",
$companyID||0] } ) do
super(*args)
end
end
This uses the fabulous rails "with_scope" functionality. With_scope
says that anything in the block that uses the command that you specify
(in this case :find, the find command) will automatically inherit the
parameters that you have specified added to any others that are passed
in. So
:conditions => ["company_id = ?", $companyID||0] }
will be added to any other conditions that are passed to the find
command!
The call to "super" obviously just passes this scoping to the parent
definition of the model's find command. I.e. we have just wrapped our
conditions around the User model's find command and we have bypassed
the authlogic method calling structure by the use of a global
variable!
And, what's more, this is NOT a cached solution, so every time that
the model's find command is called, the global variable $companyID is
re-read and the conditions re-created with the current value for that
variable.
But you will notice the
$companyID||0
part of that definition. WHy this? Well, that fixes that problem
caused by the before_filters mentioned above! You see, the $companyID
variable is not guaranteed to be set because the before_filters are
implemented before we have had the change in the user_sessions
controller to ever set the $companyID variable! So it is guaranteed to
be nil at least once!
So when it's nil, the conditions get the value 0 set and this means
that the find fails to pull back a row because in this particular
database there is no company ID for 0.
If you are using this solution, obviously you need to set a value
that's appropriate for your own dataset. Maybe -1 or -999999 will be a
correct (invalid) equivalent of our companyID 0.
Anyway, using this technique you now have a working solution!
Oh, by the way, you still do need the
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.validations_scope = :company_id
end
[.....associations, etc ....]
end
class Company < ActiveRecord::Base
authenticates_many :user_sessions
[.....associations, etc ....]
end
solution that Phatty posted in this thread because that will make sure
that authlogic will honour the user information that is unique per
company but potentially duplicated across companies. I.e. You need to
scope authlogic internnaly but what I have done above is to scope the
User model externally to authlogic as well.
Hope that this helps others out there!
Brad