adding passwords

git-svn-id: http://svn.barleysodas.com/barleysodas/trunk@84 0f7b21a7-9e3a-4941-bbeb-ce5c7c368fa7
master
andrew 2008-01-07 07:32:07 +00:00
parent a4552317d2
commit 34fe5817ed
28 changed files with 640 additions and 2 deletions

View File

@ -5,7 +5,7 @@ class SessionsController < ApplicationController
end end
def create def create
@people = People.find_by_title(params[:login]) rescue nil @people = People.authenticate(params[:login], params[:password])
if @people if @people
session[:people_title] = @people.title session[:people_title] = @people.title
session[:people_id] = @people.id session[:people_id] = @people.id

View File

@ -9,6 +9,10 @@ class People < ActiveRecord::Base
has_many :updated_pages, :class_name => 'Page', :foreign_key => 'updated_by' has_many :updated_pages, :class_name => 'Page', :foreign_key => 'updated_by'
validates_uniqueness_of :title validates_uniqueness_of :title
make_authenticatable
validates_length_of :password, :minimum => 8, :if => :password_required?,
:message => 'must be at least 8 characters in length'
## ##
# Finds me. # Finds me.
# #
@ -16,4 +20,13 @@ class People < ActiveRecord::Base
@penguincoder ||= self.find_by_title('PenguinCoder') rescue nil @penguincoder ||= self.find_by_title('PenguinCoder') rescue nil
@penguincoder @penguincoder
end end
protected
##
# Determines if the password is needed.
#
def password_required?
self.encrypted_password.blank?
end
end end

View File

@ -1,4 +1,10 @@
<p> <p>
<label for="people_title">Name</label> <%= text_field 'people', 'title' %> <label for="people_title">Name</label> <%= text_field 'people', 'title' %>
</p> </p>
<p>
<label for="people_password">Password</label> <%= text_field 'people', 'password' %>
</p>
<p>
<label for="people_password_confirmation">Password Confirmation</label> <%= text_field 'people', 'password_confirmation' %>
</p>
<%= render :partial => 'pages/page_form' %> <%= render :partial => 'pages/page_form' %>

View File

@ -2,5 +2,8 @@
<p> <p>
<label for="login">People</label> <%= text_field_tag 'login' -%> <label for="login">People</label> <%= text_field_tag 'login' -%>
</p> </p>
<p>
<label for="password">Password</label> <%= password_field_tag 'password' -%>
</p>
<%= submit_tag 'Login' %> <%= submit_tag 'Login' %>
<% end -%> <% end -%>

View File

@ -3,10 +3,13 @@ class CreatePeoples < ActiveRecord::Migration
create_table :peoples do |t| create_table :peoples do |t|
t.column :title, :string t.column :title, :string
t.column :role_id, :integer t.column :role_id, :integer
t.column :encrypted_password, :string, :limit => 512
t.column :salt, :string, :limit => 512
end end
add_index :peoples, :title add_index :peoples, :title
add_index :peoples, :role_id add_index :peoples, :role_id
p = People.new :title => 'PenguinCoder', :page => Page.new p = People.new :title => 'PenguinCoder', :page => Page.new,
:password => 'new_password', :password_confirmation => 'new_password'
p.role = Role.admin_role p.role = Role.admin_role
p.save p.save
end end

View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) <2006> <Luke Redpath>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,45 @@
Crypted Authentication Plugin for Rails
=======================================
This is an ultra-lightweight, simple crypted authentication plugin for Rails.
It is extracted from several projects I've worked on - code that has been written many times over and over again making it an ideal candidate for extraction to a plugin. The plugin also has good test coverage. The tests were written as BDD-style specifications although I chose Test::Unit over RSpec for portability reasons
== Using the plugin
Using the plugin is really simple. Simply add the following call to your model:
class User < ActiveRecord::Base
make_authenticatable
end
The plugin doesn't assume too much. All it requires in your schema is username, encrypted_password and salt columns. The plugin does not handle validations for you - it leaves you to decide how you want to validate but here is a good example that I use in one of my applications:
class User < ActiveRecord::Base
make_authenticatable
validates_presence_of :username, :message => 'cannot be blank'
validates_length_of :password, :minimum => 6, :if => :password_required?,
:message => 'must be at least 6 characters in length'
protected
def password_required?
self.encrypted_password.blank?
end
end
As well as the three attributes listed above, the plugin defines a standard object attribute called password. This stores the clear-text password and is not persisted to the database. The salt and encrypted_password attributes are marked as attr_protected which means you cannot assign values to them using mass assignment (such as new and create hashes). An authenticatable? instance method can be used in your own tests to assert that the plugin functionality has been added to your model.
== Authentication
Finally, a class-level authenticate method can be used to find a user for a given username and password. This function returns nil if no user is found.
if user = User.authenticate(params[:username], params[:password])
flash[:notice] = 'Logged in successfully']
session[:current_user] = user.id
redirect_to home_url
end
== Feedback
Please send any feedback to Luke Redpath <contact@lukeredpath.co.uk>. This plugin is licensed under the MIT license.

View File

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the crypted_authentication plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the crypted_authentication plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'CryptedAuthentication'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@ -0,0 +1,5 @@
author: Luke Redpath
email: contact@lukeredpath.co.uk
license: MIT
homepage: http://opensource.agileevolved.com
summary: Yet Another Rails Authentication Plugin - or YARAP - a simple crypted authentication plugin complete with a full BDD-style test-suite. Adds basic authentication to your models.

View File

@ -0,0 +1,2 @@
# mixin functionality to activerecord
ActiveRecord::Base.send(:include, CryptedAuthentication::Authenticator)

View File

@ -0,0 +1 @@
# Install hook code here

View File

@ -0,0 +1,55 @@
# Crypted Authentication plugin
# (c) Luke Redpath <contact[AT]lukeredpath.co.uk>
#
# A simple crypted authentication plugin for Rails
require 'digest/sha1'
module CryptedAuthentication
module Authenticator
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def make_authenticatable
attr_accessor :password, :password_confirmation
attr_protected :encrypted_password, :salt
before_save :encrypt_password
end
def authenticate(username, password)
user = self.find_by_title(username)
return nil if user.nil?
user.matches_password?(password) ? user : nil
end
end
def authenticatable?
true
end
def matches_password?(cleartext_password)
self.encrypted_password == encrypt(cleartext_password, self.salt)
end
protected
def encrypt_password
return if self.password.blank? or self.password_confirmation.blank?
if self.password != self.password_confirmation
self.errors.add(:passwords, 'do not match')
return
end
generate_salt
self.encrypted_password = encrypt(self.password, self.salt)
self.encrypted_password
end
def generate_salt
self.salt = encrypt(Time.now, rand.to_s)
end
def encrypt(string, salt='')
Digest::SHA1.hexdigest("---#{string}---#{salt}---")
end
end
end

View File

@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :crypted_authentication do
# # Task goes here
# end

View File

@ -0,0 +1,19 @@
test:
adapter: sqlite3
dbfile: test.sqlite3.db
# adapter: sqlite
# dbfile: test.sqlite.db
# adapter: mysql
# host: localhost
# username:
# password:
# database: test
# adapter: postgresql
# host: localhost
# username:
# password:
# database: test

View File

@ -0,0 +1,8 @@
Rails::Initializer.run do |config|
config.cache_classes = true
config.whiny_nils = true
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_mailer.delivery_method = :test
config.action_mailer.perform_deliveries = true
end

View File

@ -0,0 +1,3 @@
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
end

View File

@ -0,0 +1,9 @@
ActiveRecord::Schema.define(:version => 2) do
create_table "test_users", :force => true do |t|
t.column "username", :string
t.column "encrypted_password", :string
t.column "salt", :string
end
end

View File

@ -0,0 +1,121 @@
ENV['DB'] = 'test'
require 'test/unit'
require File.dirname(__FILE__) + '/ptk_helper'
require File.dirname(__FILE__) + '/../lib/crypted_authentication'
def encrypt(string, salt='')
Digest::SHA1.hexdigest("---#{string}---#{salt}---")
end
class TestUser < ActiveRecord::Base
end
class ObjectWithCryptedAuthPluginIncluded < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
end
def test_should_be_authenticatable
assert @user.authenticatable?
end
def test_should_not_allow_mass_assignment_of_encrypted_password
@user.attributes = {:encrypted_password => 'foo'}
assert_nil @user.encrypted_password
end
def test_should_not_allow_mass_assignment_of_salt
@user.attributes = {:salt => 'foo'}
assert_nil @user.salt
end
end
class NewUserWithCryptedAuthPluginIncluded < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
end
def test_should_have_a_nil_password
assert_nil @user.password
end
end
class UserWithNoClearTextPassword < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
@user.password = nil
end
def test_should_not_change_salt_when_calling_save
@user.salt = 'foo'
@user.save
assert_equal 'foo', @user.salt
end
def test_should_not_change_encrypted_password_when_calling_save
@user.encrypted_password = 'password'
@user.save
assert_equal 'password', @user.encrypted_password
end
end
class CryptedAuthUserWithClearTextPassword < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
@user.password = 'password'
end
def test_should_have_a_random_salt_after_calling_save
@user.save
assert_not_nil @user.salt
end
def test_should_have_an_encrypted_password_after_calling_save
@user.save
assert_equal @user.encrypted_password, encrypt('password', @user.salt)
end
end
class UserWithEncryptedPassword < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
@user.password = 'password'
@user.save
end
def test_should_be_authenticatable_with_original_cleartext_password
assert @user.matches_password?('password')
end
def test_should_not_be_authenticatable_with_the_wrong_cleartext_password
assert !@user.matches_password?('wrongpass')
end
end
class UserAuthenticator < Test::Unit::TestCase
def setup
TestUser.send(:make_authenticatable)
@user = TestUser.new
@user.username = 'joebloggs'
@user.password = 'password'
@user.save
end
def test_should_return_a_user_for_a_matching_username_and_password
assert_equal @user, TestUser.authenticate('joebloggs', 'password')
end
def test_should_return_nil_for_nonexistent_username
assert_nil TestUser.authenticate('nouser', 'password')
end
def test_should_return_nil_for_incorrect_password
assert_nil TestUser.authenticate('joebloggs', 'wrongpassword')
end
end

View File

@ -0,0 +1,26 @@
# We are always a test environment and should never be anything else
ENV["RAILS_ENV"] ||= "test"
require File.join(File.dirname(__FILE__), 'ptk')
# Set up RAILS_ROOT to #{plugin_path}/test
unless defined?(RAILS_ROOT)
root_path = PTK::LoadPath.expand(__FILE__, '..', '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath(true).to_s
end
RAILS_ROOT = root_path
end
# add #{plugin_path}/test/lib
PTK::LoadPath.add RAILS_ROOT, 'lib'
# add #{plugin_path}/lib
PTK::LoadPath.add RAILS_ROOT, '..', 'lib'
require 'rubygems'
require 'test/unit'
require 'active_support'

View File

@ -0,0 +1,11 @@
require 'action_pack'
require 'action_controller'
require 'action_controller/test_process'
ActionController::Base.ignore_missing_templates = true
if PTK::Configuration.load :routes
ActionController::Routing::Routes.reload rescue nil
end
class ActionController::Base; def rescue_action(e) raise e end; end

View File

@ -0,0 +1,4 @@
require 'action_mailer'
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true

View File

@ -0,0 +1,27 @@
require 'active_record'
require 'active_record/fixtures'
ActiveRecord::Base.logger = Logger.new(File.join(RAILS_ROOT, 'test.log'))
# Load the database.yml from #{plugin_path}/test/config if it exists
if file = PTK::Configuration.find_path(:database)
config = YAML::load_file(file)
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
# Load a schema if it exists
if schema = PTK::Configuration.find_path(:schema)
load(schema)
# Setup fixtures if the directory exists
if fixtures = PTK::Configuration.find_path(:fixtures)
PTK::LoadPath.add fixtures
Test::Unit::TestCase.fixture_path = fixtures
Test::Unit::TestCase.use_instantiated_fixtures = false
end
end
end

View File

@ -0,0 +1,31 @@
# We require the initializer to setup the environment properly
unless defined?(Rails::Initializer)
if File.directory?("#{RAILS_ROOT}/../../../rails")
require "#{RAILS_ROOT}/../../../rails/railties/lib/initializer"
else
require 'rubygems'
require_gem 'rails'
require 'initializer'
end
Rails::Initializer.run(:set_load_path)
end
# We overwrite load_environment so we can have only one file
module Rails
class Initializer
def load_environment
end
end
end
# We overwrite the default log to be a directory up
module Rails
class Configuration
def default_log_path
File.join(root_path, "#{environment}.log")
end
end
end
# We then load it manually
PTK::Configuration.load :environment

View File

@ -0,0 +1,148 @@
require 'singleton'
module PTK
class Configuration
class << self
def load(config, fatal = false)
if (file = PTK::PathSet.instance.send(config)) == :ignore then false
elsif File.exists?(file)
require file
true
elsif fatal then raise LoadError, "PTK could not find #{file}"
else
STDERR.puts "PTK::WARNING: could not find #{file}"
false
end
end
def find_path(config, fatal = false)
if (file = PTK::PathSet.instance.send(config)) == :ignore then false
elsif File.exists?(file) then file
elsif fatal then raise LoadError, "PTK could not find #{file}"
else
STDERR.puts "PTK::WARNING: could not find #{file}"
false
end
end
def draw
yield PTK::PathSet.instance
end
end
end
class PathSet
include Singleton
attr_accessor :ptk_prefix
attr_accessor :config
attr_accessor :fixtures
attr_accessor :environment
attr_accessor :schema
attr_accessor :database
attr_accessor :routes
def initialize
self.ptk_prefix = 'ptk'
self.config = File.join(RAILS_ROOT, 'config')
self.fixtures = File.join(RAILS_ROOT, 'fixtures')
self.environment = File.join(self.config, 'environment.rb')
self.database = File.join(self.config, 'database.yml')
self.schema = File.join(self.config, 'schema.rb')
self.routes = File.join(self.config, 'routes.rb')
end
end
class Initializer
# The init.rb in the root directory of the plugin will be loaded by default
attr_accessor :init
# The specific environmental frameworks of a plugin, such as needing the ActionController
# ActionMailer or ActiveRecord gems to be preloaded. A special requirement called
# 'environment' will load tests as though they were in the test environment of a normal
# Rails application.
attr_accessor :frameworks
def frameworks
[@frameworks].flatten
end
# Suites are test extensions including assertions and various tools for easier testing
attr_accessor :suites
def suites
[@suites].flatten
end
# A container for the PathSet instance
attr_reader :paths
def initialize
self.init = true
self.frameworks = :none
self.suites = :all
@paths = PTK::PathSet.instance
end
def self.run(command = :process)
initializer = PTK::Initializer.new
yield initializer if block_given?
initializer.send(command)
end
def process
initialize_frameworks
initialize_suites
initialize_plugin
end
def initialize_frameworks
return if frameworks.include?(:none)
self.frameworks = [:rails] if frameworks.include?(:rails)
frameworks.each { |lib| require_ptk File.join('gem', lib.to_s) }
end
def initialize_suites
return if suites.include?(:none)
self.suites = all_suites if suites.include?(:all)
suites.each { |lib| require_ptk File.join('suite', lib.to_s) }
end
def initialize_plugin
return unless self.init
require File.join(RAILS_ROOT, '..', 'init')
end
protected
def all_suites
Dir.glob(File.join(RAILS_ROOT, 'lib', 'ptk', 'suite', '*.rb')).inject([]) do |a, file|
a << File.basename(file, '.rb').to_sym
a
end
end
def require_ptk(library)
file = paths.ptk_prefix.empty? ? library : File.join(paths.ptk_prefix, library)
require file
end
end
class LoadPath
def self.expand(file, *dirs)
File.join(*([File.expand_path(File.dirname(file))] << dirs))
end
def self.add(*dirs)
path = File.expand_path(File.join(*dirs))
$:.unshift path
$:.uniq!
end
end
end

View File

@ -0,0 +1,23 @@
class Test::Unit::TestCase
# http://project.ioni.st/post/217#post-217
#
# def test_new_publication
# assert_difference(Publication, :count) do
# post :create, :publication => {...}
# # ...
# end
# end
#
# modified by mabs29 to include arguments
def assert_difference(object, method = nil, difference = 1, *args)
initial_value = object.send(method, *args)
yield
assert_equal initial_value + difference, object.send(method, *args), "#{object}##{method}"
end
def assert_no_difference(object, method, *args, &block)
assert_difference object, method, 0, *args, &block
end
end

View File

@ -0,0 +1,10 @@
require 'action_controller/test_process'
require 'ujs/controller_methods'
class ControllerStub < ActionController::Base
def index
render :nothing => true
end
end
ControllerStub.send(:include, UJS::ControllerMethods)

View File

@ -0,0 +1,29 @@
# Do not comment out this line; it sets the RAILS_ROOT constant and load paths, not Rails itself
require File.join(File.dirname(__FILE__), 'lib', 'ptk', 'boot')
PTK::Initializer.run do |setup|
# You can also redefine the paths of certain directories and files, namely:
#setup.paths.config = File.join(RAILS_ROOT, 'config')
#setup.paths.fixtures = File.join(RAILS_ROOT, 'fixtures')
#setup.paths.database = File.join(setup.paths.config, 'database.yml')
#setup.paths.schema = File.join(setup.paths.config, 'schema.rb')
#setup.paths.routes = File.join(setup.paths.config, 'routes.rb')
#setup.paths.environment = File.join(setup.paths.config, 'environment.rb')
# If any of these paths are set to ':ignore', no warnings will appear if they are missing.
# Frameworks are the gems from Rails which you need PTK to load for your plugin.
# The special :rails framework creates a fully-fledged Rails environment and requires
# the environment.rb file.
# Valid options are: :action_controller, :action_mailer, :active_record, :rails
setup.frameworks = :active_record # :active_record, :action_controller
# Extra libraries of assertions and other common methods that provide more testing
# utilities. To hand-pick which suites you want, uncomment the below
#setup.suites = :difference
# If for some particular reason you do not want your plugin's init to be called
# at the end of this block, uncomment the below:
setup.init = true
end

View File

@ -0,0 +1 @@
# Uninstall hook code here