find_by_login - How to use more than one field (company & login)

41 views
Skip to first unread message

bradkieser

unread,
Aug 5, 2009, 12:04:59 PM8/5/09
to Authlogic
Authlogic assumes a single ID field (email or login).

Can anyone suggest how I can implement using two fields, not just one?
We have a company and well as login. Logins are unique within a
company but not within the overall system (e.g. company ABC will have
only one "billSmith" by company DEF can also have a "billSmith" as a
login).

So having login and password alone is not good enough. I need company,
login and password

Any suggestions on how to do this anyone?

phattymatty

unread,
Aug 6, 2009, 10:05:13 AM8/6/09
to Authlogic
You'll want to use validations_scope to do this. Look into this and
authenticates_many for details.

You may have something like this in your models:

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

This much should allow you to create the same user/email for different
companies.

Your sessions controller will have to be updated to reflect that you
are scoping the user to a company for the new, create, and destroy
actions. How you implement this in your app is up to you. Hope this
helps.

JL Smith

unread,
Aug 6, 2009, 12:05:32 PM8/6/09
to Authlogic
Thanks for the help phatty!

bradkieser

unread,
Aug 7, 2009, 5:52:10 AM8/7/09
to Authlogic


On Aug 6, 3:05 pm, phattymatty <phattymatt...@gmail.com> wrote:
> You'll want to use validations_scope to do this. Look into this and
> authenticates_many for details.
>
> You may have something like this in your models:
>
> 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
>
> This much should allow you to create the same user/email for different
> companies.
>
> Your sessions controller will have to be updated to reflect that you
> are scoping the user to a company for the new, create, and destroy
> actions. How you implement this in your app is up to you. Hope this
> helps.
>

Wow.that is awesome help and I can't thank you enough! I was on the
point of forking the code to add a company field but this is such an
elegant solution!

You rock, Phatty! Thank you!

bradkieser

unread,
Aug 12, 2009, 9:16:07 AM8/12/09
to Authlogic
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


On Aug 6, 3:05 pm, phattymatty <phattymatt...@gmail.com> wrote:

JL Smith

unread,
Aug 12, 2009, 9:35:30 AM8/12/09
to Authlogic
Your solution relies on a global company id variable? I just don't
like that.

Before Ben Johnson switched over to his new blog, I could've sworn he
posted some kind of example about how to scope your logins for this
very use case. I don't see it anymore...but surely there's a better
way than relying on a global.
Reply all
Reply to author
Forward
0 new messages