Search code examples
rubysequel

Reproduce Sequel's validates_min_length validation


I'm hoping to reproduce Sequel's validates_min_length errors for a Bcrypt-encrypted password. I can't use the validation as it will test the password hash, rather than the un-encrypted text.

I'm having difficulty getting the password= method to produce the desired errors.

Un-encrypted-password logic

require 'sequel'

DB = Sequel.sqlite

DB.create_table(:users) do
  primary_key :id
  String :name, null: false, unique: true
  String :password, null: false
end

class User < Sequel::Model
  plugin :validation_helpers

  def validate
    super
    validates_presence [:name,:password]
    validates_unique [:name]
    validates_min_length 8, :password
  end

end

IRB:

irb(main):001:0> u=User.new(name: 'foobar', password: 'Pa55w0rd')
=> #<User @values={:name=>"foobar", :password=>"Pa55w0rd"}>
irb(main):002:0> u.valid?
=> true
irb(main):003:0> u.password=nil
=> nil
irb(main):004:0> u
=> #<User @values={:name=>"foobar", :password=>nil}>
irb(main):005:0> u.valid?
=> false
irb(main):007:0> u.errors
=> {:password=>["is not present", "is shorter than 8 characters"]}
irb(main):008:0> u.password='foo'
=> "foo"
irb(main):009:0> u
=> #<User @values={:name=>"foobar", :password=>"foo"}>
irb(main):010:0> u.valid?
=> false
irb(main):011:0> u.errors
=> {:password=>["is shorter than 8 characters"]}

Encrypted-password logic

require 'sequel'
require 'bcrypt'

DB = Sequel.sqlite

DB.create_table(:users) do
  primary_key :id
  String :name, null: false, unique: true
  String :password_hash, null: false
end

class User < Sequel::Model
  plugin :validation_helpers
  include BCrypt

  def validate
    super
    validates_presence [:name,:password]
    validates_unique [:name]
  end

  def password
    # check for :password_hash existence to ensure that validates_presence :password works correctly
    @password ||= Password.new(password_hash) if password_hash
  end

  def password=(new_password)

    # add validation errors
    errors.add(:password, 'is shorter than 8 characters') if new_password==nil || new_password.length < 8

    if new_password == nil
      @password = nil 
    else
      @password = Password.create(new_password)
    end

    self.password_hash = @password

  end

end

IRB:

irb(main):001:0> u=User.new(name: 'foobar', password: 'Pa55w0rd')
=> #<User @values={:name=>"foobar", :password_hash=>"$2a$10$K3UALPYz/bb5bdrGmbq22eRM31A6rU3kqkbzcU4.6J5APQVSqxQo6"}>
irb(main):002:0> u.valid?
=> true
irb(main):003:0> u.password=nil
=> nil
irb(main):004:0> u
=> #<User @values={:name=>"foobar", :password_hash=>nil}>
irb(main):005:0> u.valid?
=> false
irb(main):006:0> u.errors
=> {:password=>["is not present"]}
irb(main):007:0> u.password='foo'
=> "foo"
irb(main):009:0> u
=> #<User @values={:name=>"foobar", :password_hash=>"$2a$10$lA6fsKXSvl5cd.Zl53qEqOzxk1LPehvGujWaXwcf1//IUc82CmowC"}>
irb(main):008:0> u.valid?
=> true

Both invalid passwords (nil,'foo') are missing the is shorter than 8 characters error.

What am I missing?

Versions:

$ sequel --version
sequel 5.7.1

Solution

  • This works as desired:

    require 'sequel'
    require 'bcrypt'
    
    DB = Sequel.sqlite
    
    DB.create_table(:users) do
      primary_key :id
      String :name, null: false, unique: true
      String :password_hash, null: false
    end
    
    class User < Sequel::Model
      plugin :validation_helpers
      include BCrypt
    
      def validate
        super
        validates_presence [:name,:password]
        validates_unique [:name]
        errors.add(:password, 'is shorter than 8 characters') if @password==nil || @password.length < 8
      end
    
      def password
        Password.new(password_hash) if password_hash
      end
    
      def password=(new_password)
    
        # uncomment to prevent bad passwords from changing a good password; probably needs to include a non-terminating error message
        # return if new_password==nil || new_password.length < 8
    
        # store clear-text password for validation
        @password = new_password
    
        #modify password hash accordingly
        self.password_hash = if new_password then Password.create(new_password) else nil end
      end
    
    end