In this conclusion to a five-part series that delves into the Rails framework's Active Record, you'll finish learning about validations, and take a look at callbacks. This article is excerpted from chapter five of the book Beginning Rails: From Novice to Professional, written by Jeffrey Allan Hardy, Cloves Carneiro Jr. and Hampton Catlin (Apress; ISBN: 1590596862).
Callbacks and the Active Record - Updating the User Model (Page 5 of 5 )
Our Event model is nicely filled out, but we still need to do a little bit of work on our User model. We’ll be applying a lot of the techniques we described in this chapter, such as custom methods to allow us to perform user authentication, and validation methods to make sure our data stays clean.
When we created the user migration (Listing 5-2), we added a field called password. This field stored a plain-text password, which if you think about it, isn’t very secure. It’s always a good idea to encrypt any sensitive data so it can’t be easily read by would-be intruders. We’ll deal with the encryption in the User model itself, but the first thing we’ll do is rename the field in the database from password to hashed_password. This is so we can create a custom accessor called password with which we can set the password while maintaining a field to store the encrypted version in the database. The plain-text password will never be saved.
To accomplish this, we’ll create a migration. From the terminal, issue the following command to create the new migration:
Next, fill in the migration as shown in Listing 5-28.
Listing 5-28.Migration to Rename password to hashed_password in db/migrate/007_ rename_ password_to_hashed_password.rb
class RenamePasswordToHashedPassword < ActiveRecord::Migration def self.up rename_column :users, :password, :hashed_password end
def self.down rename_column :users, :hashed_password, :password end end
Run the migration using therake db:migratecommand as follows:
$ rake db:migrate
Next, update yourUsermodel so that it looks like Listing 5-29. In Listing 5-29, we’ve programmed all the user authentication methods we’ll need for allowing users to log in. Let’s take a look at the code first, and then we’ll describe in detail what we’ve done.
Listing 5-29. Current User Model, in app/models/user.rb
require 'digest/sha1'
class User < ActiveRecord::Base attr_accessor :password
def self.authenticate(login, password) user = find_by_login(login) return user if user && user.authenticated?(password) end
def authenticated?(password) hashed_password == encrypt(password) end
protected def encrypt_new_password return if password.blank? self.hashed_password = encrypt(password) end
def password_required? hashed_password.blank? || !password.blank? end
def encrypt(string) Digest::SHA1.hexdigest(string) end end
Whenever you’re storing something sensitive, like a password, you want to encrypt it. To encrypt the password in ourUsermodel, we use a simple algorithm called a hash that will create a random-looking string from the provided input. This hashed output cannot be turned back into the original string easily, so even if someone steals your database, he will have a prohibitively difficult time discovering your users’ passwords. Ruby has a built-in library calledDigest, which includes many hashing algorithms.
Let’s go through the additions to ourUser model:
require 'digest/sha1': We start by requiring theDigestlibrary we will use for encrypting the passwords. This loads the needed library and makes it available to work with in our class.
attr_accessor :password: This defines an accessor attribute,password, at the top of the class body. This tells Ruby to create reader and writer methods forpassword. Since the password column doesn’t actually exist in our table anymore, apassword method won’t be created automatically by Active Record. Still, we need a way to set the password before it’s encrypted, so we make our own attribute to use. This will work just like any model attribute, except that it won’t be persisted to the database when the model is saved.
before_save :encrypt_new_password: Thisbefore_savecallback tells Active Record to run theencrypt_new_passwordmethod before it saves a record. That means it will apply to all operations that trigger a save, includingcreateandupdate.
encrypt_new_password: This method should perform encryption only if thepassword attribute contains a value, since we wouldn’t want it to happen unless a user is changing her password. So, if thepassword attribute is blank, we return from the method and thehash_passwordvalue is never set. If thepasswordvalue is not blank, we have some work to do. We set thehashed_passwordattribute to the encrypted version of the password by laundering it through theencryptmethod.
encrypt: This method is fairly simple. It leverages Ruby’sDigestlibrary that we included on the first line to create an SHA1 digest of whatever we pass it. Since methods in Ruby always return the last thing evaluated,encryptwill return the encrypted string.
password_required?: When we’re performing our validations, we want to make sure we’re validating the presence, length, and confirmation of the password only if validation is required. And it’s required only if this is a new record (thehashed_passwordattribute is blank) or if thepasswordaccessor we created has been used to set a new password (!password.blank?). To make this easy, we’ve created thepassword_required?predicate method, which returnstrueif a password is required, orfalseif it’s not. We then apply this method as an:ifcondition on all our password validators.
self.authenticate: You can tell this is a class method because it’s prefixed withself (it’s defined on the class itself). That means you don’t access it via an instance; you access it directly off the class, just as you would withfind,new, orcreate (User.authenticate, not @user = User.new; @user.authenticate). Theauthenticatemethod accepts a login and an unencrypted password. It uses a dynamic finder (find_by_login) to fetch the user with a matching login. If the user was found, the user variable will contain aUserobject; if not, it will benil. Knowing this, we can return the value ofuserif, and only if, it is notniland theauthenticated?method returnstruefor the given password (user && user.authenticated?(password)).
authenticated?: This is a simple predicate method that checks to make sure the storedhashed_passwordmatches the given password after it has been encrypted (viaencrypt). If it matches,trueis returned.
Let’s play with these new methods from the console so you can get a better idea of how this comes together.
So, when we ask theUser model to authenticate someone, we pass in the login and the plain-text password. Theauthenticatemethod hashes the given password and then compares it to the stored (hashed) password in the database. If the passwords match, theUser object is returned and authentication was successful. When we try to use an incorrect password,nilis returned. In Chapter 6, we’ll write code in our controller to use these model methods and actually allow users to log in to the site. But for now, we have a properly built and secure back end for how users will authenticate.
Summary
After reading this chapter, you should have a complete understanding of Active Record models. We’ve covered associations, conditions, validations, and callbacks at breakneck speed. Now the fun part starts. In the next chapter you will get to use all the groundwork that we established in this chapter to produce the web interface for the data structures we have created here. This is when you really get to reap the benefits of your hard work.
DISCLAIMER: The content provided in this article is not warranted or guaranteed by Developer Shed, Inc. The content provided is intended for entertainment and/or educational purposes in order to introduce to the reader key ideas, concepts, and/or product reviews. As such it is incumbent upon the reader to employ real-world tactics for security and implementation of best practices. We are not liable for any negative consequences that may result from implementing any information covered in our articles or tutorials. If this is a hardware review, it is not recommended to open and/or modify your hardware.