Let's say I have a simple model like so with a presence validation on the password field.
class User < ApplicationRecord validates :password, presence: trueend
If I try to update the password to a blank, the validation fails. It acts the same whether the record object was just create or if it was loaded from the database. All normal.
cuser = User.create!(name: "Foo", password: "abc123") TRANSACTION (0.0ms) begin transaction User Create (0.5ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:47:34.844263"], ["updated_at", "2024-06-21 18:47:34.844263"]] TRANSACTION (0.1ms) commit transactioncuser.update!(name: "Bar", password: "") Validation failed: Password can't be blank (ActiveRecord::RecordInvalid)fuser = User.last User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]fuser.update!(name: "Other", password: "") Validation failed: Password can't be blank (ActiveRecord::RecordInvalid)
If I only use has_secure_password
...
class User < ApplicationRecord has_secure_passwordend
user.update!(name: "Bar", password: "")
works for a created and found record. The password change is ignored. This is not a documented feature of has_secure_password
, and maybe it should be, but that's not what I'm asking about.
cuser = User.create!(name: "Foo", password: "abc123") TRANSACTION (0.1ms) begin transaction User Create (0.6ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:56:38.988813"], ["updated_at", "2024-06-21 18:56:38.988813"]] TRANSACTION (0.1ms) commit transactioncuser.update!(name: "Bar", password: "") TRANSACTION (0.1ms) begin transaction User Update (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Bar"], ["updated_at", "2024-06-21 18:56:55.334524"], ["id", 21]] TRANSACTION (0.1ms) commit transactionfuser = User.last User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]fuser.update!(name: "Other", password: "") TRANSACTION (0.1ms) begin transaction User Update (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Other"], ["updated_at", "2024-06-21 18:57:06.994792"], ["id", 21]] TRANSACTION (0.1ms) commit transaction
Look what happens if I use both together.
class User < ApplicationRecord has_secure_password validates :password, presence: trueend
user.update!(name: "Bar", password: "")
works, but only if the record was just created. If the record was found, the validation fails.
cuser = User.create!(name: "Foo", password: "abc123") TRANSACTION (0.1ms) begin transaction User Create (0.5ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:46:26.576676"], ["updated_at", "2024-06-21 18:46:26.576676"]] TRANSACTION (1.2ms) commit transactioncuser.update!(name: "Bar", password: "") TRANSACTION (0.1ms) begin transaction User Update (0.3ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Bar"], ["updated_at", "2024-06-21 18:46:26.579666"], ["id", 17]] TRANSACTION (0.1ms) commit transactionfuser = User.last User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]fuser.update!(name: "Other", password: "") Validation failed: Password can't be blank (ActiveRecord::RecordInvalid)
That's very strange behavior. It's as if has_secure_password
is actively suppressing the password presence validation, but only if #previously_new_record?
is true. I understand why it might want to suppress the validation (a warning or error about conflicting validations would be better), but why only for newly created objects?
Is this a bug in has_secure_password
? Or is there some reason for it to behave this way?
To reproduce...
rails new
rails g model user name:string password:string
- Uncomment
gem "bcrypt"
in your Gemfile.
Ruby 3.2.2Rails 7.1.3.4Gemfile.lock
For context, see How to prevent password from being updated if empty in Rails Model and test it with RSpec?.