Skip to content

Instantly share code, notes, and snippets.

@bricker
Created July 10, 2012 04:34
Show Gist options
  • Save bricker/3081028 to your computer and use it in GitHub Desktop.
Save bricker/3081028 to your computer and use it in GitHub Desktop.
has_secure_attribute, a smarter `has_secure_password` for Rails.
# This is a modified versions of Rails' built-in `has_secure_password`
# It allows you to do the same thing to any attribute.
# Useful for legacy databases, credit card information, addresses, etc.
# Use it pretty much the same way you would use has_secure_password
# You can also use any name for the digest column,
# just pass in an :encrypted_attribute option:
#
# has_secure_attribute :credit_card_number, encrypted_attribute: :ccn_hash
#
# will require a "ccn_hash" column in your database, and will add a
# `credit_card_number` attribute to your model.
# Make sure to add the sensitive attribute to your filter parameters!
module ActiveModel
module SecureAttribute
extend ActiveSupport::Concern
module ClassMethods
def has_secure_attribute(attribute_name, options = {})
gem 'bcrypt-ruby', '~> 3.0.0'
require 'bcrypt'
cattr_accessor :encrypted_attribute, :attribute
self.attribute = attribute_name.to_sym
self.encrypted_attribute = options.fetch(:encrypted_attribute, "#{attribute_name}_digest").to_sym
attr_reader attribute
if options.fetch(:validations, true)
validates_confirmation_of attribute
validates_presence_of attribute, :on => :create
end
before_create { raise "#{encrypted_attribute.to_s.gsub("_", " ").capitalize} missing on new record" if send(encrypted_attribute).blank? }
extend ClassMethodsOnActivation
include InstanceMethodsOnActivation
attribute_writer_method(attribute)
if respond_to?(:attributes_protected_by_default)
def self.attributes_protected_by_default
super + [send(:encrypted_attribute).to_s]
end
end
end
end
module ClassMethodsOnActivation
private
def attribute_writer_method(name)
define_method("#{name}=") do |unencrypted_attribute|
unless unencrypted_attribute.blank?
instance_variable_set("@#{self.class.attribute}", unencrypted_attribute)
send("#{self.class.encrypted_attribute}=", BCrypt::Password.create(unencrypted_attribute))
end
end
end
end
module InstanceMethodsOnActivation
def authenticate(unencrypted_attribute)
BCrypt::Password.new(send(self.class.encrypted_attribute)) == unencrypted_attribute && self
end
end
end
end
# These specs don't pass because they're poorly written, not because of the code above.
require "spec_helper"
describe ActiveModel::SecureAttribute do
pending "broken - need to unit test independent of app" do
it "is invalid with blank passw on create" do
user = build :admin_user, passw: ""
user.should_not be_valid
end
it "nil passw" do
user = build :admin_user, passw: nil
user.should_not be_valid
end
it "blank passw doesn't override previous passw" do
user = build :admin_user, passw: 'test'
user.passw = ''
user.passw.should eq 'test'
end
it "passw must be present" do
user = build :admin_user, passw: ''
user.should_not be_valid
user.errors.keys.should eq [:passw]
end
it "match confirmation" do
user = build :admin_user, passw: 'correct', passw_confirmation: "wrong"
user.should_not be_valid
user.errors.keys.should eq [:passw]
user.passw_confirmation = "correct"
user.should be_valid
end
it "authenticate" do
user = create :admin_user, passw: "secret"
user.authenticate("wrong").should be_false
user.authenticate("secret").should eq user
end
it "#passw_digest should be protected against mass assignment" do
pending
#AdminUser.active_authorizers[:default].should be_a ActiveModel::MassAssignmentSecurity::BlackList
#AdminUser.active_authorizers[:default].should include :passw_digest
end
it "mass_assignment_authorizer should be WhiteList" do
pending
#AdminUser.stub(:attr_accessible) { :name }
#active_authorizer = AdminUser.active_authorizers[:default]
#active_authorizer.should be_a ActiveModel::MassAssignmentSecurity::WhiteList
#active_authorizer.should_not include :passw_digest
#active_authorizer.should include :name
#assert active_authorizer.include?(:name)
end
it "User should not be created with blank digest" do
pending
# user = build :admin_user, passw: ""
# user.stub!(:passw_digest) { "" }
# -> { user.save }.should raise_error
#
# user.passw = "secret"
# -> { user.save }.should_not raise_error
end
it "humanizes the column name for error" do
pending
# user = build :admin_user, passw: ""
#
# begin
# AdminUser.create(user)
# rescue RuntimeError => e
# e.message.should match "Encrypted passw missing on new record"
# end
end
it "recogizes the column name passed in as an attribute" do
build(:admin_user).methods.should include :passw_digest
end
it "recognizes the attribute name passed in as an attribute" do
build(:admin_user).methods.should include :passw
end
it "adds column name passed in to attributes_protected_by_default" do
pending
# AdminUser.send(:attributes_protected_by_default).should include "passw_digest"
end
end # pending
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment