开发者

Rails Authlogic authentication method

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 'log开发者_StackOverflow中文版in_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 :)


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.


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


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!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜