ActiveRecord is great if your database schema evolves along with your web app from birth, but not all of us have this luxury. Many of us live in the world of corporate IT – a world of legacy databases and bureaucracies that make getting a Rails app into production hard enough, let alone getting a new schema into production. DataMapper is a common alternative ORM to use for these scenarios. DataMapper is ideally suited for legacy databases, as Martin Gamsjaeger describes:
-
DataMapper allows you to map meaningful model and property names to cryptic legacy table and column naming conventions. It allows you to do so either on a per model/property, or an app wide basis.
-
DataMapper supports lazy properties that will only be fetched when actually accessed.
-
DataMapper has seamless support for composite primary keys.
-
DataMapper only cares about the properties (columns) you explicitly declare in your models. Other columns will never be touched or read.
-
DataMapper works nicely with foreign key constraints in your database and with the help of dm-constraints it also supports creating them.
There’s some relevant documentation on http://datamapper.org/docs/legacy too.
I’m not going into depths with DM in this article; there are plenty of tutorials out there. What I am going to demonstrate is how to live with a legacy database written for an app with a horribly insecure authentication mechanism, based on a schema whose table and column names don’t match their Rails Model counterparts.
Here’s our User model:
class User include DataMapper::Resource include DataMapper::MassAssignmentSecurity devise :database_authenticatable, :authentication_keys => [:username] storage_names[:default] = 'legacy_User_table' property :id, Serial, :field => 'UserId', :required => true property :username, String, :field => 'LoginId', :required => true property :encrypted_password, String, :field => 'PasswordSHA1Hash', :required => true property :enabled, Integer, :field => 'Enabled', :required => true property :is_admin, Integer, :field => 'IsSuperAdmin', :required => true property :first_name, String, :field => 'Name', :required => true property :last_name, String, :field => 'Surname', :required => true attr_accessible :username, :password, :password_confirmation def password_salt=(password_salt) end def password_salt end def password_digest(password) self.class.encryptor_class.digest(password) end end
You’ll notice this model overrides :authentication_keys, using :username instead of :email. I also map the table name to ‘legacy_Users_table’ since we don’t have a conveniently named ‘users’ table in our schema. Our password in this monstrosity is stored as an unsalted SHA1 hash, which then gets Base64-encoded. Really secure, huh?
For Devise to work with unsalted passwords, I’ve had to override the password_salt functions and the password digest function that Devise looks for. Here, this lives in a custom Devise encryptor class, which I define in an initializer called devise_encryptor.rb:
module Devise module Encryptors class Sha1base64 < Base def self.digest(password) sha1 = Digest::SHA1.digest(password) Base64.strict_encode64(sha1) end def self.salt(username) nil end end end end
This encryptor takes the password, short-circuits the salt function, and returns the Base64-encoded SHA1 hash. There are a couple of configuration changes needed to wire this up. In /initializers/devise.rb, set:
config.encryptor = :sha1base64
This will reference the above custom encryptor class name.
There is one last workaround we have to apply to get the password_salt override in our user model to work. From the Devise source code: we have to tell Devise not to apply the schema in ORMs where the Devise declaration and schema belongs to the same class (as Datamapper and Mongoid). This goes inside devise.rb, in the Devise.config block, and is courtesy of Jared Morgan in the DataMapper mailing list:
config.apply_schema = false
The last change you’ll make is in the devise sign_in view, which you likely generated using rails g devise:views. Use the :username instead of :email for your login credentials:
<%= f.label :username %> <%= f.text_field :username %>
If all goes well, you’ll now be able to log into your shiny new Rails app, backended by a steaming pile of crap designed and maintained by monkeys.