Rails Authlogic authentication method - ruby-on-rails
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 :)
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!
Related
How can I save a field as other field in other table?
I have an authentication feature that provides me the username of the current user. I also have a table of events (created by users). And when a user creates an event, how could I save a field called host with the current name of this user in the table events?
The concept is called "deserialization", which is why I made my own gem for this: quickery. Using quickery class Event < ApplicationRecord belongs_to: user quickery { user: { username: :host } } end Using Normal-Way class Event < ApplicationRecord belongs_to :user before_save do host = user.username end end class User < ApplicationRecord has_many :events # you may want to comment out this `after_destroy`, if you don't want to cascade when deleted after_destroy do events.update_all(host: nil) end # you may want to comment out this `after_update`, if you don't want each `event.host` to be updated (automatically) whenever this `user.username` gets updated after_update do events.update_all(host: username) end end Usage Example (for either above) user = User.create!(username: 'foobar') event = Event.create!(user: user) puts event.host # => 'foobar'
Or, if your Event doesn't belongs_to :user, then you'll need to update this manually in the controller as follows class EventsController < ApplicationController def create #event = Event.new #event.assign_attributes(event_params) #event.host = current_user.username if #event.save # success, do something UPDATE THIS else # validation errors, do something UPDATE THIS end end def update #event = Event.find(params[:id]) #event.assign_attributes(event_params) #event.host = current_user.username if #event.save # success, do something UPDATE THIS else # validation errors, do something UPDATE THIS end end private def event_params params.require(:event).permit(:someattribute1, :someattribute2) # etc. UPDATE THIS end end
Rails devise create a related object after new user is created?
Using Rails 4.1 and Devise 3.0.3, how can I create an associated object on User when User is instantiated and connect the two? def User has_one :case end def Case belongs_to :user end In this case, User is set up with devise. What I'd like to do is instantiate an object like so: #case = Case.new current_user.case = #case or current_user.case << #case How can I execute this code when a request is made to "registrations#new"?
Here is how I would implement this: class User < ActiveRecord::Base ... before_action :create_case, only: [:new, :create] def create_case case = Case.create self.case = case.id # Maybe check if profile gets created and raise an error # or provide some kind of error handling end end
Override create action of Devise::RegistrationsController and pass a block to it: # app/controllers/registrations_controller.rb class RegistrationsController < Devise::RegistrationsController def create super do resource.case = Case.new resource.save end end end
You can use create method inside your User model. Something similar like this: def self.create(username, email, pass) user = User.new(:username => username, :email => email, :password => pass) user.case = Case.new return user if user.save end
I think that you have to override Devise::RegistrationsController#create and in the create action you have to take resource and do something like this resource.build_case https://github.com/plataformatec/devise/blob/master/app/controllers/devise/registrations_controller.rb http://guides.rubyonrails.org/association_basics.html#has_one-association-reference
New object belonging_to won't associate with Devise User who created it
When my user creates an object that should have his id on it, the id comes out nil. Ideas why? # controllers/units_controller.rb class UnitsController < ApplicationController before_filter :authenticate_user! def new #unit = Unit.new #unit.user = current_user # also tried .user_id = current_user.id [...] # models/unit.rb class Unit < ActiveRecord::Base belongs_to :user # :user has_many :units too [...] The object is saved successfully, but with an empty user_id field (showing nil). No exception is thrown. There is no callback on save.
If you're doing this in the standard Rails two step (#new (POST)-> #create), then you need to associate Unit with current_user in the #create action. def create #unit = Unit.new #unit.user = current_user # ... end
Using the save method together with update_attributes. Is this common?
A user can sign up as an artist. All the user needs to do now, is provide his email. In Artist controller, def create. Is it normal to have something like: def create #artist = current_user respond_to do |format| if #artist.update_attributes(params[:user]) # params[:user] contains email #artist.is_artist = true #artist.save .... In my User model, I have: attr_accessible :email Which means, I can't simply do #artist.update_attributes(:is_artist => true). I would have to use the save method instead. Is this type of approach common? Or is there a better way?
You can define before_create method in your model: class User < ActiveRecord::Base ... before_create :fill_fields def fill_fields is_artist = true end end
I would do the following: 1st: I wound not set up an ArtistController if you do not have an Artist Model. rather I would add a non-restful method in your UserController, and push the implemention logic into the model ... # config/routes.rb resources :users do member {post 'signup_as_artist'} end # UserController def signup_as_artist #user = User.find(params[:id]) #user.signup_as_artist end # User def signup_as_artist self.update_attribute :is_artist, true end Good luck
Protect sensitive attributes with declarative_authorization
What's a cool way to protect attributes by role using declarative_authorization? For example, a user can edit his contact information but not his role. My first inclination was to create multiple controller actions for different scenarios. I quickly realized how unwieldy this could become as the number of protected attributes grows. Doing this for user role is one thing, but I can imagine multiple protected attributes. Adding a lot controller actions and routes doesn't feel right. My second inclination was to create permissions around specific sensitive attributes and then wrap the form elements with View hepers provided by declarative_authorizations. However, the model and controller aspect of this is a bit foggy in my mind. Suggestions would be awesome. Please advise on the best way to protect attributes by role using declarative_authorizations.
EDIT 2011-05-22 Something similar is now in Rails as of 3.1RC https://github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb so I would suggest going that route now. ORIGINAL ANSWER I just had to port what I had been using previously to Rails 3. I've never used declarative authorization specifically, but this is pretty simple and straightforward enough that you should be able to adapt to it. Rails 3 added mass_assignment_authorizer, which makes this all really simple. I used that linked tutorial as a basis and just made it fit my domain model better, with class inheritance and grouping the attributes into roles. In model acts_as_accessible :admin => :all, :moderator => [:is_spam, :is_featured] attr_accessible :title, :body # :admin, :moderator, and anyone else can set these In controller post.accessed_by(current_user.roles.collect(&:code)) # or however yours works post.attributes = params[:post] lib/active_record/acts_as_accessible.rb # A way to have different attr_accessible attributes based on a Role # #see ActsAsAccessible::ActMethods#acts_as_accessible module ActiveRecord module ActsAsAccessible module ActMethods # In model # acts_as_accessible :admin => :all, :moderator => [:is_spam] # attr_accessible :title, :body # # In controller # post.accessed_by(current_user.roles.collect(&:code)) # post.attributes = params[:post] # # Warning: This frequently wouldn't be the concern of the model where this is declared in, # but it is so much more useful to have it in there with the attr_accessible declaration. # OHWELL. # # #param [Hash] roles Hash of { :role => [:attr, :attr] } # #see acts_as_accessible_attributes def acts_as_accessible(*roles) roles_attributes_hash = Hash.new {|h,k| h[k] ||= [] } roles_attributes_hash = roles_attributes_hash.merge(roles.extract_options!).symbolize_keys if !self.respond_to? :acts_as_accessible_attributes attr_accessible write_inheritable_attribute :acts_as_accessible_attributes, roles_attributes_hash.symbolize_keys class_inheritable_reader :acts_as_accessible_attributes # extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods) include InstanceMethods unless included_modules.include?(InstanceMethods) else # subclass new_acts_as_accessible_attributes = self.acts_as_accessible_attributes.dup roles_attributes_hash.each do |role,attrs| new_acts_as_accessible_attributes[role] += attrs end write_inheritable_attribute :acts_as_accessible_attributes, new_acts_as_accessible_attributes.symbolize_keys end end end module InstanceMethods # #param [Array, NilClass] roles Array of Roles or nil to reset # #return [Array, NilClass] def accessed_by(*roles) if roles.any? case roles.first when NilClass #accessed_by = nil when Array #accessed_by = roles.first.flatten.collect(&:to_sym) else #accessed_by = roles.flatten.flatten.collect(&:to_sym) end end #accessed_by end private # This is what really does the work in attr_accessible/attr_protected. # This override adds the acts_as_accessible_attributes for the current accessed_by roles. # #see http://asciicasts.com/episodes/237-dynamic-attr-accessible def mass_assignment_authorizer attrs = [] if self.accessed_by self.accessed_by.each do |role| if self.acts_as_accessible_attributes.include? role if self.acts_as_accessible_attributes[role] == :all return self.class.protected_attributes else attrs += self.acts_as_accessible_attributes[role] end end end end super + attrs end end end end ActiveRecord::Base.send(:extend, ActiveRecord::ActsAsAccessible::ActMethods) spec/lib/active_record/acts_as_accessible.rb require 'spec_helper' class TestActsAsAccessible include ActiveModel::MassAssignmentSecurity extend ActiveRecord::ActsAsAccessible::ActMethods attr_accessor :foo, :bar, :baz, :qux acts_as_accessible :dude => [:bar], :bra => [:baz, :qux], :admin => :all attr_accessible :foo def attributes=(values) sanitize_for_mass_assignment(values).each do |k, v| send("#{k}=", v) end end end describe TestActsAsAccessible do it "should still allow mass assignment to accessible attributes by default" do subject.attributes = {:foo => 'fooo'} subject.foo.should == 'fooo' end it "should not allow mass assignment to non-accessible attributes by default" do subject.attributes = {:bar => 'baaar'} subject.bar.should be_nil end it "should allow mass assignment to acts_as_accessible attributes when passed appropriate accessed_by" do subject.accessed_by :dude subject.attributes = {:bar => 'baaar'} subject.bar.should == 'baaar' end it "should allow mass assignment to multiple acts_as_accessible attributes when passed appropriate accessed_by" do subject.accessed_by :bra subject.attributes = {:baz => 'baaaz', :qux => 'quuux'} subject.baz.should == 'baaaz' subject.qux.should == 'quuux' end it "should allow multiple accessed_by to be specified" do subject.accessed_by :dude, :bra subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'} subject.bar.should == 'baaar' subject.baz.should == 'baaaz' subject.qux.should == 'quuux' end it "should allow :all access" do subject.accessed_by :admin subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'} subject.bar.should == 'baaar' subject.baz.should == 'baaaz' subject.qux.should == 'quuux' end end
To me this filtering problem is something that should be applied at the controller level. You'll want to have something somewhere that defines how to decide which attributes are writeable for a given user. # On the user model class User < ActiveRecord::Base # ... # Return a list of symbols representing the accessible attributes def self.allowed_params(user) if user.admin? [:name, :email, :role] else [:name, email] end end end Then, in the application controller you can define a method to filter parameters. class ApplicationController < ActionController::Base # ... protected def restrict_params(param, model, user) params[param].reject! do |k,v| !model.allowed_params(user).include?(k) end end # ... end And finally in your controller action you can use this filter: class UserController < ActionController::Base # ... def update restrict_params(:user, User, #current_user) # and continue as normal end # ... end The idea is that you could then define allowed_params on each of your models, and have the controllers for each of these use the same filter method. You could save some boilerplate by having a method in application controller that spits out a before filter, like this: def self.param_restrictions(param, model) before_filter do restrict_params(param, model, #current_user) if params[param] end end # in UserController param_restrictions :user, User These examples are intended to be illustrative rather than definitive, I hope they help with the implementation of this.
I'd use scoped_attr_accessible, which looks like just what you're looking for. Only you need to set the scope at the start of a request for all models. To do that, use a before_filter in your application_controller.rb: before_filter do |controller| ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role end
I would avoid every solution based on user access in model because it seems potentially dangerous. I would try this approach: class User < ActiveRecord::Base def update_attributes_as_user(values, user) values.each do |attribute, value| # Update the attribute if the user is allowed to #user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute) end save end def modifiable_attributes admin? ? [:name, :email, :role] : [:name, :email] end end Then in your controller change your update action from: #user.update_attributes(params[:user]) to #user.update_attributes_as_user(params[:user], current_user)
Rails 3.1+ comes with a +assign_attributes+ method for this purpose - http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes.