Reference: "Agile Web Development with Rails (4th Edition)" for Rails 3
Walkthrough Depot application with errata and changes for using MySQL on OS X.
I did not wish to veer too much from the text (e.g. starting with Nginx/Phusion), but Homebrew and RVM are essential.
- Command Line Tools
- Git 1.7.11.2
- Redis 2.4.15
- MySQL 5.5.27
- Ruby 1.9.3 (via RVM)
- redis
- minitest
- mysql2
- rails 3.0.17
rails new agile_web_dev_depot -d mysql
rake db:create # create databases
rake db:migrate
Copyrighted book description test data... Seriously?
A bit more orientation, I am typing changes -- except for the seed data and css files. I want the experience of creating the application, and fixing errors. Last year, when attempting to get through this, Iteration A2 was where the waters got rough. Perhaps the css has been tweaked, or I have become more savvy, but now, index view styling worked easily.
No changes.
When first working through the text, I had trouble with testing, and made notes about the changes required (as found via Errata or Stack Overflow).
Perhaps starting off with minitest installed was a mistake, I should have made a clean gemset for this project...
# test_helper.rb
require 'rails/test_help'
require 'minitest' # add me
# gemfile
group :test do
# Pretty printed test output
gem 'turn', :require => false
gem 'minitest' # add me
end
No changes.
No changes.
No changes.
Getting price into line_items table seemed important enough... And changing LineItem model to use appropriate pricing.
No changes... F5 iteration was added to capture testing changes, rather than appending those to F4.
Adding price to line_item (E Playtime) requires changes to order functional testing.
# order_controllers_test.rb
test "should get new" do
cart = Cart.create
session[:cart_id] = cart.id
LineItem.create(:cart => cart,
:product => products(:ruby),
:price => products(:ruby).price)
get :new
assert_response :success
end
Also, update line_items fixture to include pricing.
# line_items.yml
one:
product: ruby
price: 49.50
order: one
two:
product: ruby
price: 49.50
cart: one
No changes.
Iteration does not really require changes, but... Sorting orders by created_at might not give the results shown, as the generated orders may all have the same timestamp -- sort on order key to obtain the results shown in the text.
def index
@orders = Order.paginate :page => params[:page], :order => 'id desc',
:per_page => 10
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @orders }
end
end
No changes.
Section Email Configuration is informative, but in reality, if you are relying on email (e.g. running a business) -- you should use an email marketing service. I have worked with Mad Mimi, and the assistance offered by such services is remarkable (or Mad Mimi seemed so, your mileage may differ).
Integration test fails due to mail.from match, this version works:
# check notification email
mail = ActionMailer::Base.deliveries.last
assert_equal ["[email protected]"], mail.to
assert_equal ["[email protected]"], mail.from
assert_equal "Pragmatic Store Order Confirmation", mail.subject
No changes.
Abstract: A trip down the rabbit hole of security theater... Best practiced salted SHA hashing is inadequate. Here is the infamous explanation -- How To Safely Store A Password.
If you are reading this, you might have oddly shaped die (with die-cast figurines), and a weary wariness of blog title hype... Alas, here is the opposing view: Don't use bcrypt -- an adjacent facet of the original article.
And the punchline, hail the new normal.
I suspect brute-force attacks on a Rails site or its compromised data store are rare compared to an individual account being exploited. I would add application activity logging -- using production logs to figure out what an attacker did while masquerading as Mr. Unfortunate feels like a chore.
But in the interest of being exemplary acolytes of Hacker News, let's fix our hashing...
-
Update gemfile to include required bcrypt-ruby gem
gem "bcrypt-ruby"
-
Install new bundle
bundle install
-
Restart server
Be sure to restart the application server.
-
Update User model
require 'bcrypt'
class User < ActiveRecord::Base after_destroy :ensure_an_admin_remains validates :name, :presence => true, :uniqueness => true
validates :password, :confirmation => true attr_accessor :password_confirmation attr_reader :password
validate :password_must_be_present
def ensure_an_admin_remains if User.count.zero? raise "Can't delete last user" end end
def User.authenticate(name, password) if user = find_by_name(name) if BCrypt::Password.new(user.hashed_password) == password user end end end
def User.encrypt_password(password) # beware, not idempotent (includes salt) BCrypt::Password.create(password) end
def password=(password) @password = password if password.present? self.hashed_password = self.class.encrypt_password(password) end end
private
def password_must_be_present errors.add(:password, "Missing password") unless hashed_password.present? end end
-
Update users fixture
Make sure there is more than one record in the fixture, to avoid violating validation when controller attempts to delete user.
one:
name: dave
hashed_password: <%= User.encrypt_password('secret') %>
two:
name: gossamer
hashed_password: <%= User.encrypt_password('hair-raising hare') %>
- Make tests work (e.g. rake test)
Depending on how much you researched the bcrypt gem, or how well you copied example code, this is where you make the tests work.
You may have started testing via rails console, after doing something like this...
User.delete_all
User.create(:name => 'sean', :password => 'secret')
But then found login would not work because authenticate returned nil (if we agree encrypt_password was simple to update).
bcrypt was implemented such that it relies on object initialization and comparison. New recreates the Password object, which uses a comparison method (aka "==") to generate a digest based on its salt and the given plain-text password -- which is to say...
Password_instance == password # words great
password == Password_instance # ruh-roh
(If you are new to rails, you have to exit and invoke console whenever you want to test changes made in your application).
-
Eliminate salt column
rails generate migration RemoveSaltFromUser salt:string rake db:migrate
Be sure to update user show.html.erb view.
<p id="notice"><%= notice %></p>
<p>
<b>Name:</b>
<%= @user.name %>
</p>
<p>
<b>Hashed password:</b>
<%= @user.hashed_password %>
</p>
<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>
- Prevent mischief
What more could possibly be worth doing here?
Update User model to prevent denial of service attack...
class User < ActiveRecord::Base
PASSWORD_MIN_LENGTH = 6
PASSWORD_MAX_LENGTH = 42
# ...
validates :password, :confirmation => true
validates_length_of :password,
:minimum => PASSWORD_MIN_LENGTH,
:maximum => PASSWORD_MAX_LENGTH
# ...
def User.authenticate(name, password)
if password.length < PASSWORD_MAX_LENGTH
if user = find_by_name(name)
if BCrypt::Password.new(user.hashed_password) == password
user
end
end
end
end
bcrypt is used to ensure authentication is slower than SHA hash checking. An attacker could attempt to log in with a large password, thus generating deleterious/costly load on the server.
No changes.
Text should have explained removal of get "store/index" when scoping routes by locale.
No changes.
The following quote was not accurate...
Note that we do not normally have to explicitly call I18n functions on labels, unless we want to do something special like allowing HTML entities.
Thus, the order form was adjusted to add more translations...
<%= form_for(@order) do |f| %>
# ...
<div class="field">
<%= f.label :name, t('.name') %><br />
<%= f.text_field :name, :size => 40 %>
</div>
<div class="field">
<%= f.label :address, t('.address_html') %><br />
<%= f.text_area :address, :rows => 3, :cols => 40 %>
</div>
<div class="field">
<%= f.label :email, t('.email') %><br />
<%= f.email_field :email, :size => 40 %>
</div>
<div class="field">
<%= f.label :pay_type, t('.pay_type') %><br />
<%= f.select :pay_type, Order::PAYMENT_TYPES,
:prompt => t('.pay_prompt_html') %>
</div>
<div class="actions">
<%= f.submit t('.submit') %>
</div>
<% end %>
Figure 15.5 conflicts with the es.yml configuration for pay_type, fix the configuration to use "Forma de pago" (to be consistent with later translations).
Also, this appeared to be inaccurate...
Note that there is no need to provide English equivalents for this, because those messages are built in to Rails.
I would love to know how this got weird, but to get error messages working, the Spanish configuration included:
# es.yml
activerecord:
models:
order: "pedido"
attributes:
order:
address: "Direccón"
name: "Nombre"
email: "E-mail"
pay_type: "Forma de pago"
errors:
messages:
inclusion: "no está incluido en la lista"
blank: "no puede quedar en blanco"
errors:
template:
body: "Hay problemas con los siguientes campos:"
header:
one: "1 error ha impedido que este %{model} se guarde"
other: "%{count} errores han impedido que este %{model} se guarde"
And the English configuration included:
# en.yml
activerecord:
models:
order: "order"
errors:
template:
body: "There were problems with the following fields:"
header:
one: "1 error prohibited this %{model} from being saved"
other: "%{count} errors prohibited this %{model} from being saved"
No changes.
Skipped.
This seems a core weakness of the text, tool choice and platform support issues. I have worked with Nginx and Passenger, and installation/permissions problems can be an intense hassle.
The obvious way forward is Vagrant. I have been reading Copeland and Burns Deploying Rails, and it is a good reference from the DevOps perspective.