Let's say I have a simple model like so with a presence validation on the password field.
class User < ApplicationRecord
validates :password, presence: true
end
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 transaction
cuser.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_password
end
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 transaction
cuser.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 transaction
fuser = 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: true
end
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 transaction
cuser.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 transaction
fuser = 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.2 Rails 7.1.3.4 Gemfile.lock
For context, see How to prevent password from being updated if empty in Rails Model and test it with RSpec?.