views:

490

answers:

2

Within Authlogic, is there a way that I can add conditions to the authentication method? I know by using the find_by_login_method I can specify another method to use, but when I use this I need to pass another parameter since the find_by_login_method method only passes the parameter that is deemed the 'login_field'.

What I need to do is check something that is an association of the authentic model.. Here is the method I want to use

  # make sure that the user has access to the subdomain that they are 
  # attempting to login to, subdomains are company names
  def self.find_by_email_and_company(email, company)
    user = User.find_by_email(email)

    companies = []
    user.brands.each do |b|
      companies << b.company.id
    end

    user && companies.include?(company)
  end

But this fails due to the fact that only one parameter is sent to the find_by_email_and_company method.

The company is actually the subdomain, so in order to get it here I am just placing it in a hidden field in the form (only way I could think to get it to the model)

Is there a method I can override somehow..?

Using the answer below I came up with the following that worked:

User Model (User.rb)

  def self.find_by_email_within_company(email)
    # find the user
    user = self.find_by_email(email)

    # no need to continue if the email address is invalid
    return false if user.nil?

    # collect the subdomains the provided user has access to
    company_subdomains = user.brands.map(&:company).map(&:subdomain)

    # verify that the user has access to the current subdomain
    company_subdomains.include?(Thread.current[:current_subdomain]) && user
  end

Application Controller

  before_filter :set_subdomain

  private

    def set_subdomain
      # helper that retreives the current subdomain 
      get_company

      Thread.current[:current_subdomain] = @company.subdomain
    end

User Session Model (UserSession.rb)

find_by_login_method :find_by_email_within_company

I have read a few things about using Thread.current, and conflicting namespaces.. This is a great solution that worked for me but would love to hear any other suggestions before the bounty expires, otherwise, +100 to Jens Fahnenbruck :)

+1  A: 

In your lib folder add a file with the follwing content:

class Class
  def thread_local_accessor name, options = {}
    m = Module.new
    m.module_eval do
      class_variable_set :"@@#{name}", Hash.new {|h,k| h[k] = options[:default] }
    end
    m.module_eval %{
      FINALIZER = lambda {|id| @@#{name}.delete id }

      def #{name}
        @@#{name}[Thread.current.object_id]
      end

      def #{name}=(val)
        ObjectSpace.define_finalizer Thread.current, FINALIZER  unless @@#{name}.has_key? Thread.current.object_id
        @@#{name}[Thread.current.object_id] = val
      end
    }

    class_eval do
      include m
      extend m
    end
  end
end

I found this here

Then add code in the controller like this:

class ApplicationController < ActionController
  before_filter :set_subdomain
  private
  def set_subdomain
    User.subdomain = request.subdomains[0]
  end
end

And now you can do the following in your user model (assuming your company model has a method called subdomain:

class User < ActiveRecord::Base
  thread_local_accessor :subdomain, :default => nil

  def self.find_by_email_within_company(email)
    self.find_by_email(email)
    company_subdomains = user.brands.map(&:company).map(&:subdomain)
    company_subdomains.include?(self.subdomain) && user
  end
end

And FYI:

companies = user.brands.map(&:company).map(&:subdomain)

is the same as

companies = []
user.brands.each do |b|
  companies << b.company.subdomain
end
jigfox
Thanks for the .map($:) trick as well, I'll be able to use that all over, could you explain that? Or just slap a link in here I can read up on it at..?
Rabbott
jigfox
Great, thanks! - I will accept your solution towards the end of the Bounty to give others the chance to respond as well, as I cant change it once I've accepted.
Rabbott
Matt Briggs
@Matt, I know it worked, but now I know how! Thanks
jigfox
Have you guys seen this? http://www.ruby-forum.com/topic/161089 yikes!
Rabbott
+1  A: 

Authlogic provides API for dealing with sub domain based authentication.

class User < ActiveRecord::Base
  has_many :brands
  has_many :companies, :through => :brands
  acts_as_authentic
end

class Brand < ActiveRecord::Base
  belongs_to :user
  belongs_to :company
end

class Company < ActiveRecord::Base
  has_many :brands
  has_many :users, :through => :brands
  authenticates_many :user_sessions, :scope_cookies => true
end

Session controller:

class UserSessionsController < ApplicationController      

  def create
    @company = Company.find(params[:user_session][:company])
    @user_session = @company.user_sessions.new(params[:user_session])
    if @user_session.save 
    else
    end    
  end
end

On the other hand

Here is a way to solve the problem using your current approach(I would use the first approach):

Set custom data - to the key email of the hash used to create the UserSession object. AuthLogic will pass this value to find_by_login method. In the find_by_login method access the needed values.

Assumption: The sub domain id is set in a field called company in the form.

class UserSessionsController < ApplicationController      

  def create
    attrs = params[:user_session].dup #make a copy
    attrs[:email] = params[:user_session] # set custom data to :email key

    @user_session = UserSession.new(attrs)
    if @user_session.save 
    else
    end    
  end
end

Model code

Your code for finding the user with the given email and subdomain can be simplified and optimized as follows:

class User < ActiveRecord::Base
  def find_by_email params={}
   # If invoked in the normal fashion then ..
   return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)

   User.first(:joins => [:brands => :company}],
     :conditions => ["users.email = ? AND companies.id = ?", 
                      params[:email], params[:company]])
  end
end

Edit 1

Once the user is authenticated, system should provide access to authorized data.

If you maintain data for all the domains in the same table, then you have to scope the data by subdomain and authenticated user. Lets say you have Post model with company_id and user_id columns. When a user logs in you want to show user's posts for the sub domain. This is one way to scope user's data for the subdomain:

Posts.find_by_company_id_and_user_id(current_company, current_user)

Posts.for_company_and_user(current_company, current_user) # named scope

If you do not scope the data, you will have potential security holes in your system.

KandadaBoggu
How safe is it to have the company as a form field (Hidden I'm guessing), because with Firebug I can show hidden fields and modify their contents. So if a user changes the value in the form field for login, but then once logged in they get information specific to the subdomain, this could cause issues.. no?
Rabbott
Even if the user changes the sub domain, he will be allowed to login ONLY if he has an account in the sub domain. Calls after authentication should ensure user is accessing the authorized content (which is required anyways).
KandadaBoggu
once logged in the content is scoped to the subdomain, without heavy authorization checks you can't always check the content on every page load.. So what I could change company in the form to a company i have access too, which would allow me to login. Then once logged in my account is no longer checked with every page load to verify that i have access to the content.
Rabbott
What is the need for the sub domain in your case?
KandadaBoggu
Scoping content. companya.appname.com allows users that have access to that company to login, and all the content is scoped to Company A
Rabbott
As long as you scope the pages by the current sub-domain and the current user your system is fine. I don't see any issue in sending company name in the hidden field for authentication.
KandadaBoggu
Darn, the first approach is broken with Rails 3. =( I really like the first approach.
jpartogi
I haven't tested this on Rails3 as I am running Rails 2.3.8. Try to file a bug at github for AuthLogic.
KandadaBoggu