4

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 :)

Rabbott
  • 4,282
  • 1
  • 30
  • 53
  • you need to swap "user && companies.include?(company)" to "companies.include?(company) && user", so User#find_by_email_and_company returns the user model instead of "true" or "false" for the Array#include? method – jigfox Apr 26 '10 at 19:12
  • 1
    Your Right about the Problems Thread.current could have. So I updated my Answer with an alternative way – jigfox Apr 26 '10 at 21:25
  • thats funny, the post you linked to is the post I read that informed me of Thread.current being bad, thanks for the help Jens! – Rabbott Apr 26 '10 at 22:23
  • Also, you may want to update your solution, your private method has no name, its just "def" incase other stumble upon this question and try to use your code – Rabbott Apr 26 '10 at 22:24
  • sorry for that. it is now corrected too. – jigfox May 02 '10 at 18:32

3 Answers3

2

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.

Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • 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 May 02 '10 at 16:31
  • 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). – Harish Shetty May 02 '10 at 16:34
  • 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 May 02 '10 at 17:02
  • What is the need for the sub domain in your case? – Harish Shetty May 02 '10 at 17:36
  • 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 May 02 '10 at 18:54
  • 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. – Harish Shetty May 02 '10 at 20:25
  • 1
    Darn, the first approach is broken with Rails 3. =( I really like the first approach. – Joshua Partogi Jul 02 '10 at 17:00
  • I haven't tested this on Rails3 as I am running Rails 2.3.8. Try to file a bug at github for AuthLogic. – Harish Shetty Jul 02 '10 at 18:37
1

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
  • 18,057
  • 3
  • 60
  • 73
  • 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 Apr 26 '10 at 22:34
  • http://ruby-doc.org/core/classes/Array.html#M002189 .map(&:method_name) is the short form for .map{|o| o.method_name } I'm not sure if this is Ruby, or if it is a part of the Rails Core Extensions. So I don't know if it works in plain Ruby – jigfox Apr 26 '10 at 23:09
  • 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 Apr 28 '10 at 04:16
  • 1
    & means "treat this as a block". since a symbol isn't a block, it tries calling the to_proc method on it. to_proc on a symbol will return a proc that takes an object, and will call whatever method the symbol is on it. http://pragdave.pragprog.com/pragdave/2005/11/symbolto_proc.html – Matt Briggs May 02 '10 at 20:57
0

With rails 3 you can use this workaround:

class UserSessionsController < ApplicationController
...
def create
    @company = <# YourMethodToGetIt #>
    session_hash = params[:user_session].dup
    session_hash[:username] = { :login => params[:user_session][:username], :company => @company }
    @user_session = UserSession.new(session_hash)

    if @user_session.save
        flash[:notice] = "Login successful!"
        redirect_back_or_default dashboard_url
    else
        @user_session.username = params[:user_session][:username]
        render :action => :new
    end

...
end

Then

class UserSession < Authlogic::Session::Base
    find_by_login_method :find_by_custom_login
end

and

class User < ActiveRecord::Base
...
    def self.find_by_custom_login(hash)
        if hash.is_a? Hash
            return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
              find_by_email_and_company_id(hash[:login], hash[:company].id)
        else
            raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
        end
    end
...
end

Which is quite plain and "correct". I take me a lot of time to find out, but it works fine!

Enrico Carlesso
  • 6,818
  • 4
  • 34
  • 42