How to skip has_secure_password validations - ruby-on-rails

In my app, only admins can create new User records. The user is emailed an activation link where they set their password.
I'd like to use the has_secure_passord method (railscast):
class User < ActiveRecord::Base
has_secure_password
...
end
Works great, but it automatically validates presence of password digest...so when the admin creates the record the validations fail. I there a way to skip just the automatically added password_digest validation without skipping the others I've added?

Since 4.X versions of rails, has_secure_password takes an option :validations. If you set it to false, it will not run validations.
The 3.X version of the gem does not support this parameter. However you can backport the activemodel/lib/active_model/secure_password.rb from latest 4.0.XRC code which supports this argument.
So with that, your code will look like this:
class User < ActiveRecord::Base
has_secure_password :validations => false
...
end

I decided to do my own custom authentication. The following solution will validate passwords but only when they are being set. This allows admins to create users without adding a password.
class User < ActiveRecord::Base
include BCrypt
attr_accessor :password, :password_confirmation
validates :password, length: (6..32), confirmation: true, if: :setting_password?
def password=(password)
#password = password
self.password_hash = Password.create(password)
end
def authenticate(password)
password.present? && password_hash.present? && Password.new(password_hash) == password
end
private
def setting_password?
password || password_confirmation
end
end
If someone posts an answer that allows me to still use the has_secure_password method, I'll accept it instead.

There's no way to skip the validation, but it would be easy enough to write your own version of the method that allows you to pass an argument to determine whether or not to validate presence of the password_digest field.
Just extend ActiveModel the same way they do in the SecurePassword module (via ActiveSupport::Concern) and add your own secure password method.
i.e.
module ActiveModel
module MySecurePassword
extend ActiveSupport::Concern
module ClassMethods
def my_has_secure_password(validate_password_digest=true)
# you custom logic
end
end
end
end

Piggybacking off of Journeyer and Tybro's answers I wanted to see if you can add a private method that returns true or false to the validations hash. It worked and I used it for a different use case but in this context it would look something like this.
class User < ActiveRecord::Base
has_secure_password validations: !:setting_password
#If setting_password is true then this will be the same as
validations: false
.....
private
def setting_password?
#Some logic that determines if a user is setting a password and resolves
to true or false
password || password_confirmation
end
...
end

Related

Devise: Add Hashed Column

I'm using Devise within my Rails app and I want to create a column where a user's pin number is stored (and is hashed).
The pin number is optional so it will be empty by default and changeable on the user settings page, provided by Devise.
I have the following code which works as expected:
class RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
def account_update_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :current_password)
end
end
I would add :pin_number as one of the permitted parameters in the account_update_params function, but how would I hash that and where?
I am a novice with rails so I would be grateful to be shown how to hash the parameter and then save it with the appropriate user.
You could use the bcrypt gem for this (Devise is already using this). In your create / update actions, you would have something that looks like this:
pin_number = BCrypt::Password.create("1234")
You'd then store pin_number in the database as is. When you retrieve it, you'd make a new Bcrypt::Password object like this:
hashed_pin = #user.pin_number
pin_number = BCrypt::Password.new(hashed_pin)
You could then compare a raw string to the BCrypt::Password objects with the == operator (the string will be automatically hashed before comparison):
hashed_pin == "1234" # true
In order to use this with your User model, you would need to add the field to your User migration:
# devise_create_users.rb
t.string :pin_number
You could then access it just like any other attribute of your model:
#user = User.find(1)
#user.pin_number = BCrypt::Password.create("1234")
#user.save
You can use attr_encrypted gem for this.
#user.rb
Class User < ActiveRecord::Base
attr_encrypted :pin_number, :key => 'a secret key'
end
And save encrypted version of pin_number in encrypted_pin_number field and call it like #user.encrypted_pin_number which returns the encrypted value of pin_number.

How do I validate certain fields with rails devise on registration only

I have a set of custom fields attached to a devise model called Entrant.
I have two forms, one for registration form (three fields) and one which sits in the account area (12 fields). Most of the custom fields area required but only within the form the sits in the account area.
How do I achieve this?
I am using rails 4.2 and ruby 2.1
You can simply specify validations on actions, that is:
validates :name, presence: true, on: :create # which won't validate presence of name on update action
If you ask where to put your custom fields, then generate devise's views and update corresponding ones with these fields.
There are several ways! You could do conditional validations, for instance
class Entrant < ActiveRecord::Base
validate :foo, if: :account_area?
def account_area?
!new_record? # Assumes that Entrant that has already been saved
# is in the account area
end
end
However, it sounds like your needs are advanced enough that you should consider making a Form Object
A form object is an object that accepts parameters, performs validations on that data, then saves a model instance.
class AccountForm
include ActiveModel::Model
include Virtus # Provides AR like attribute functionality and mass assignment
def initialize(entrant)
#entrant = entrant
end
attribute :foo, String
validates :foo, presence: true # This is only used on the account page, so no need to mess with conditional logic
def save
if valid?
persist!
true
else
false
end
end
def persist!
#entrant.update_attributes(foo: self.foo)
end
end
This is just a great example of how non-rails-specific object oriented programming can make your life easier and your app more maintainable. Make a class like above, stick it in app/forms and restart your server. Then in your controller, you'll just pass it the model
class EntrantController < ApplicationController
def update
#form = Form.new(Entrant.find(params[:id]))
#form.attributes = params[:entrant]
if #form.save
redirect_to some_path
else
render "edit"
end
end
end
By default devise only asks for a combination of email/password, you can add other fields by adding a sanitizer (see there -> Devise how to add a addtional field to the create User form?).
If you want to add other fileds to validate, you should create a secondary Entrant controller and add a specific callback to your model.
Typically:
after_update :validate_entrant_form, if: :property_changed?
I hope this will help you.
validates :name, presence: true, if: :condition_holds?
def condition_holds?
# some code here that evaluates to a boolean
end
Maybe this way help you.
Add attribute in devise model : say attr_accessor :validate_certain. In your controller action, devise model instance say #user have to update like this #user.validate_certain = true. and change your appropriate validation conditions in devise model
validates :name, presence: true, if: :validate_certain_changed?
def validate_certain_changed?
validate_certain.present?
end
When I have to do something like this I like to think of it as it validates if something in in the field but you can also take a nil value
Entrant.validates_presence_of(:foo, :allow_nil => true)
I also have this concern when using devise on customer with forms on separate pages updating different set of customer fields
I believe most of the solution works but I was looking for the simplest, easiest and foolproof way to implement the solution
Thus came this.
validates :phone, :country, :postal_code, :street_address, presence: true, allow_nil: true
The allow_nil: true instruct the model to validate the fields ONLY if it exists on the submitted form. If you want more protection, you can use extra para like :on => :update

on an ActiveModel Object, how do I check uniqueness?

In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects to abstract away multi-layer forms and stop using accepts_nested_attributes_for.
Edit: see below for a solution.
I've almost exactly duplicated his code sample, as I had the same problem to solve:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations doesn't have a uniqueness validator, as it's supposed to be a non-database backed variant of ActiveRecord.
I figured there are three ways to handle this:
Write my own method to check this (feels redundant)
Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
Or add the constraint in the data storage layer
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
I could do something like (metaprogramming, I would need to modify some other areas):
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
But now I have two checks running in my class, first I use valid? and then I use a rescue statement for the data storage constraints.
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
A simplified approach...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
#klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
You can use it in your ActiveModel classes like so:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model class has validations as well. Those validations aren't run when you call Signup.new.save, so you will have to check those some other way. You can always use save(validate: false) inside the above persist! method, but then you have to make sure all validations are in the Signup class, and keep that class up to date, when you change any validations in Account or User.

has_secure_password authenticate inside validation on password update

I'm using has_secure_password in a User model. I have implemented a way for users to change their password outside of the model, but to keep things DRY, I'm trying to move the validations needed from the controller to the model.
The User model looks something like this:
class User
include Mongoid::Document
include ActiveModel::SecurePassword
has_secure_password
field: :password_digest, type: String
attr_accessible :password, :password_confirmation, :current_password
end
Users change their passwords by submitting the following:
user[current_password] - Currently stored password
user[password] - New password
user[password_confirmation] - New password confirmation
I'm using update_attributes(params[:user]) on the User model for the current user. My problem is that calling update_attributes updates the password_digest before using validations, so the following code won't work:
def password_validation_required?
password_digest.blank? || !password.blank? || !password_confirmation.blank?
end
validate(on: :update, if: :password_validation_required?) do
unless authenticate(current_password)
add(:current_password, 'invalid password')
end
end
authenticate is authenticating based on the new password_digest generated from user[password]. Is there an elegant way to access the old password_digest value for authentication? One idea I had was to re-query the user to gain access to another authenticate method that will authenticate against the old password_digest value. The problem is that it's not a clean solution.
I think this one's a bit cleaner than #Parazuce's:
validate :validates_current_password
private
def validates_current_password
return if password_digest_was.nil? || !password_digest_changed?
unless BCrypt::Password.new(password_digest_was) == current_password
errors.add(:current_password, "is incorrect")
end
end
The password_digest field has ActiveModel::Dirty methods associated with it, so I decided to go with:
validate(on: :update, if: :password_validation_required?) do
unless BCrypt::Password.new(password_digest_was) == current_password
errors.add(:current_password, "is incorrect")
end
end
This prevents the need to override password= with additional logic which could introduce bugs in the future if other features used password=.

Rails: How to validate data manually using a given model?

I am creating a Tumblr alternative to learn how use Rails. I am at the authentication part, and I decided to do it from scratch. I want to allow users to log in using either their username or their email. The user logs in via the Sessions controller but I need to verify if the login is either a valid username or a valid email. So I need to validate data in the Sessions controller using the User model.
I found this answer on SO: How do I validate a non-model form in Rails 3? but it will force me to duplicate the validations. Is that the only way to do it, or is there another way that is cleaner?
The best option I can imagine is creating a module and then including it at your User model and at the object you're going to use for the form:
module AuthenticationValidation
def self.included( base )
base.validates :username, :presence => true
base.validates :email, :presence => true
# add your other validations here
end
end
And then include this module at your models:
class User < ActiveRecord::Base
include AuthenticationValidation
end
class LoginForm
include ActiveModel::Validations
include ActiveModel::Conversion
include AuthenticationValidation
attr_accessor :username, :email
end
And then you have avoided repeating the validation itself.

Resources