How can Authlogic be used with Mongoid? - ruby-on-rails

Authlogic requires many Active Record features that are not available in Active Model and thus not available in Mongoid. How can Authlogic be used with Mongoid?

It is possible for Authlogic to be used with Mongoid by monkey patching Authlogic. YMMV.
First, add both Authlogic and Mongoid to the Gemfile.
gem 'mongoid', github: 'mongoid/mongoid' # Currently required for Rails 4
gem 'authlogic', '~> 3.3.0'
Next monkey patch Authlogic. First make the directory lib/modules and create file lib/modules/login.rb
# lib/modules/login.rb
module Authlogic
module ActsAsAuthentic
module Login
module Config
def find_by_smart_case_login_field(login)
# Case insensitivity for my project means downcase.
login = login.downcase unless validates_uniqueness_of_login_field_options[:case_sensitive]
if login_field
where({ login_field.to_sym => login }).first
else
where({ email_field.to_sym => login }).first
end
end
end
end
end
end
Tell rails to load your new module by editing config/application.rb
# config/application.rb
module YourApp
class Application < Rails::Application
config.autoload_paths += %W(#{config.root}/lib) # ADD THIS
end
end
Tell rails to require your module by creating config/initializers/authlogic.rb and add one line:
# config/initializers/authlogic.rb
require 'modules/login.rb'
The next step is to make a concern for being authenticable. Concerns are only available by default in Rails 4. You will need to add this directly to your authenticable class in Rails 3.
# app/models/concerns/authenticable.rb
module Authenticable
extend ActiveSupport::Concern
included do
include Authlogic::ActsAsAuthentic::Base
include Authlogic::ActsAsAuthentic::Email
include Authlogic::ActsAsAuthentic::LoggedInStatus
include Authlogic::ActsAsAuthentic::Login
include Authlogic::ActsAsAuthentic::MagicColumns
include Authlogic::ActsAsAuthentic::Password
include Authlogic::ActsAsAuthentic::PerishableToken
include Authlogic::ActsAsAuthentic::PersistenceToken
include Authlogic::ActsAsAuthentic::RestfulAuthentication
include Authlogic::ActsAsAuthentic::SessionMaintenance
include Authlogic::ActsAsAuthentic::SingleAccessToken
include Authlogic::ActsAsAuthentic::ValidationsScope
end
def readonly?
false
end
module ClassMethods
def <(klass)
return true if klass == ::ActiveRecord::Base
super(klass)
end
def column_names
fields.map &:first
end
def quoted_table_name
self.name.underscore.pluralize
end
def default_timezone
:utc
end
def primary_key
if caller.first.to_s =~ /(persist|session)/
:_id
else
##primary_key
end
end
def find_by__id(*args)
find *args
end
def find_by_persistence_token(token)
where(persistence_token: token).first
end
# Replace this with a finder to your login field
def find_by_email(email)
where(email: email).first
end
def with_scope(query)
query = where(query) if query.is_a?(Hash)
yield query
end
end
end
That is basically it. Here is a sample User class.
# app/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
include Authenticable
field :name
field :email
field :crypted_password
field :password_salt
field :persistence_token
field :single_access_token
field :perishable_token
field :login_count, type: Integer, default: 0
field :failed_login_count, type: Integer, default: 0
field :last_request_at, type: DateTime
field :last_login_at, type: DateTime
field :current_login_at, type: DateTime
field :last_login_ip
field :current_login_ip
field :role, type: Symbol, default: :user
index({ name: 1 })
index({ email: 1 }, { unique: true })
index({ persistence_token: 1}, { unique: true })
index({ last_login_at: 1})
validates :name, presence: true
validates :email, presence: true, uniqueness: true
validates :crypted_password, presence: true
validates :password_salt, presence: true
acts_as_authentic do |config|
config.login_field = 'email' # Remember to add a finder for this
end
end
The UserSession would then look like this:
# app/models/user_session.rb
class UserSession < Authlogic::Session::Base
def to_key
new_record? ? nil : [self.send(self.class.primary_key)]
end
def to_partial_path
self.class.name.underscore
end
end
The application controller helpers are the same as always.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user
private
def current_user_session
#current_user_session = UserSession.find unless
defined?(#current_user_session)
#current_user_session
end
def current_user
#current_user = current_user_session &&
current_user_session.user unless defined?(#current_user)
#current_user
end
end
That is it. Everything else should work exactly as it used to.

Related

Sequel validations in concerns

I have a Sequel model like this:
class User < Sequel::Model
include Notificatable
def validate
super
validates_presence [:email]
end
end
# concerns/notificatable.rb
module Notificatable
extend ActiveSupport::Concern
included do
def validate
super
validates_presence [:phone]
end
end
end
And here I got a problem: Notificatable validate method overrides the same method in the User model. So there is no :name validations.
How can I fix it? Thanks!
Why use a concern? Simple ruby module inclusion works for what you want:
class User < Sequel::Model
include Notificatable
def validate
super
validates_presence [:email]
end
end
# concerns/notificatable.rb
module Notificatable
def validate
super
validates_presence [:phone]
end
end

Can you check previously used passwords (password history) using Authlogic?

I am using Authlogic in a rails app for password validation. I would like to ensure that the user doesn't use any of the past 10 used passwords. Does Authlogic allow you to do that, or do you have to hand roll something?
To make sure that your users dont repeat passwords you will need a password history
$ rails g migration CreatePasswordHistory
class CreatePasswordHistories < ActiveRecord::Migration
def self.change
create_table(:password_histories) do |t|
t.integer :user_id
t.string :encrypted_password
t.timestamps
end
end
end
Now you can update the users model to save the password to the password history model something like:
class AdminUser < ActiveRecord::Base
include ActiveModel::Validations
has_many :password_histories
after_save :store_digest
validates :password, :unique_password => true
...
private
def save_password_history
if encrypted_password_changed?
PasswordHistory.create(:user => self, :encrypted_password => encrypted_password)
end
end
end
Finally create a model called unique_password_validator
require 'bcrypt'
class UniquePasswordValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.password_histories.each do |password_history|
bcrypt = ::BCrypt::Password.new(password_history.encrypted_password)
hashed_value = ::BCrypt::Engine.hash_secret(value, bcrypt.salt)
record.errors[attribute] << "has been used previously." and return if hashed_value == password_history.encrypted_password
end
end
end
Hope this helps
Happy Hacking

Ruby on Rails - Does the scope of SessionsHelper reach models?

In my Organisations module, I try to assign a value to a field before creating the Organisation record.
I use a before_create filter in the model, which usually works fine.
But when I try to assign a value coming from an attribute of the current_user method defined in the session, I get an Undefined method 'current_user' error message.
As doing this works fine in a controller, I wonder why it does not work in the model?
Here is my code for the model:
# == Schema Information
#
# Table name: organisations
#
# id :integer not null, primary key
# playground_id :integer
# code :string(255)
# name :string(255)
# description :text
# parent_organisation_id :integer
# organisation_level :integer
# hierarchy :string(255)
# created_by :string(255)
# updated_by :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Organisation < ActiveRecord::Base
### before filter
before_create :set_code
before_create :set_hierarchy
validates :code, presence: true, uniqueness: true
validates :name, presence: true, uniqueness: true
validates :organisation_level, presence: true
validates :created_by , presence: true
validates :updated_by, presence: true
# validates :owner_id, presence: true
# validates :status_id, presence: true
validates :playground_id, presence: true
# belongs_to :owner, :class_name => "User", :foreign_key => "owner_id" # helps retrieving the owner name
# belongs_to :status, :class_name => "Parameter", :foreign_key => "status_id" # helps retrieving the status name
belongs_to :organisation
has_many :organisations
### private functions definitions
private
### before filters
def set_code
if Organisation.count > 0
self.code = self.organisation.code + '-' + code
end
end
def set_hierarchy
if Organisation.count == 0
self.hierarchy = current_user.current_playground_id.to_s + '.001'
else
last_one = Organisation.maximum("hierarchy")
self.hierarchy = last_one.next
end
end
end
Here is my code for the SessionsHelper (inspired from Rails Tutorial):
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
remember_token = User.encrypt(cookies[:remember_token])
#current_user ||= User.find_by(remember_token: remember_token)
end
def signed_in?
!current_user.nil?
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
end
Here is an example from a controller where the assignment works:
#business_flow.playground_id = current_user.current_playground_id
I'd be glad to understand why it does not work in the model.
The SessionsHelper module gets included into your controller class, so you can use its methods from there. It does not get included in the model class.
In fact it probably wouldn't work even if you included it by hand, because the model would need access to the cookies method for the current_user method to work. cookies comes from an ActionController method.
To achieve what you want, try making the change in your Organisation object from the controller that's creating that object, passing in the user information that the controller has access to.

Using Roles for Validations in Rails

Is it possible to use the roles used for attr_accessible and attr_protected? I'm trying to setup a validation that only executes when not an admin (like this sort of http://launchware.com/articles/whats-new-in-edge-scoped-mass-assignment-in-rails-3-1). For example:
class User < ActiveRecord::Base
def validate(record)
unless # role.admin?
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
After looking into this and digging through the source code, it appears that the role passed in when creating an Active Record object is exposed through a protected method mass_assignment_role. Thus, the code in question can be re-written as:
class User < ActiveRecord::Base
def validate(record)
unless mass_assignment_role.eql? :admin
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
Sure can would be something like this:
class User < ActiveRecord::Base
attr_accessible :role
validates :record_validation
def record_validation
unless self.role == "admin"
errors.add(:name, "error message") if ..
end
end
You could do this
class User < ActiveRecord::Base
with_options :if => :is_admin? do |admin|
admin.validates :password, :length => { :minimum => 10 } #sample validations
admin.validates :email, :presence => true #sample validations
end
end
5.4 Grouping conditional validations

Don't get `should validate_inclusion_of` to work in Rails3.2, RSpec2

Model:
class Contact < ActiveRecord::Base
validates :gender, :inclusion => { :in => ['male', 'female'] }
end
Migration:
class CreateContacts < ActiveRecord::Migration
def change
create_table "contacts", :force => true do |t|
t.string "gender", :limit => 6, :default => 'male'
end
end
end
RSpec test:
describe Contact do
it { should validate_inclusion_of(:gender).in(['male', 'female']) }
end
Result:
Expected Contact to be valid when gender is set to ["male", "female"]
Anybody has an idea why this spec doesn't pass? Or can anybody reconstruct and (in)validate it? Thank you.
I misunderstood how the .in(..) should be used. I thought I could pass an array of values, but it seems it does only accept a single value:
describe Contact do
['male', 'female'].each do |gender|
it { should validate_inclusion_of(:gender).in(gender) }
end
end
I don't really know what's the difference to using allow_value though:
['male', 'female'].each do |gender|
it { should allow_value(gender).for(:gender) }
end
And I guess it's always a good idea to check for some not allowed values, too:
[:no, :valid, :gender].each do |gender|
it { should_not validate_inclusion_of(:gender).in(gender) }
end
I usually prefer to test these things directly. Example:
%w!male female!.each do |gender|
it "should validate inclusion of #{gender}" do
model = Model.new(:gender => gender)
model.save
model.errors[:gender].should be_blank
end
end
%w!foo bar!.each do |gender|
it "should validate inclusion of #{gender}" do
model = Model.new(:gender => gender)
model.save
model.errors[:gender].should_not be_blank
end
end
You need to use in_array
From the docs:
# The `validate_inclusion_of` matcher tests usage of the
# `validates_inclusion_of` validation, asserting that an attribute can
# take a whitelist of values and cannot take values outside of this list.
#
# If your whitelist is an array of values, use `in_array`:
#
# class Issue
# include ActiveModel::Model
# attr_accessor :state
#
# validates_inclusion_of :state, in: %w(open resolved unresolved)
# end
#
# # RSpec
# describe Issue do
# it do
# should validate_inclusion_of(:state).
# in_array(%w(open resolved unresolved))
# end
# end
#
# # Test::Unit
# class IssueTest < ActiveSupport::TestCase
# should validate_inclusion_of(:state).
# in_array(%w(open resolved unresolved))
# end
#
# If your whitelist is a range of values, use `in_range`:
#
# class Issue
# include ActiveModel::Model
# attr_accessor :priority
#
# validates_inclusion_of :priority, in: 1..5
# end
#
# # RSpec
# describe Issue do
# it { should validate_inclusion_of(:state).in_range(1..5) }
# end
#
# # Test::Unit
# class IssueTest < ActiveSupport::TestCase
# should validate_inclusion_of(:state).in_range(1..5)
# end
#

Resources