Skip to content

Depot application exercise from "Agile Web Development with Rails"

Notifications You must be signed in to change notification settings

hakutsuru/agile_web_dev_depot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Depot -- Agile Web Development with Rails

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.


Configuration

Xcode 4.4.1

  • Command Line Tools

Homebrew 0.9.3

  • Git 1.7.11.2
  • Redis 2.4.15
  • MySQL 5.5.27

RVM 1.15.8

  • Ruby 1.9.3 (via RVM)

Gems

  • redis
  • minitest
  • mysql2
  • rails 3.0.17

Iteration Descriptions

Chapter 06 -- Creating the Application

A1 -- Creating the Products Maintenance Application

A2 -- Making Prettier Listings

Chapter 07 -- Validation and Unit Testing

B1 -- Validating!

B2 -- Unit Testing of Models

Chapter 08 -- Catalog Display

C1 -- Creating the Catalog Display

C2 -- Adding a Page Layout

C3 -- Using a Helper to Format the Price

C4 -- Functional Testing of Controllers

Chapter 09 -- Cart Creation

D1 -- Finding a Cart

D2 -- Connecting Products to Carts

D3 -- Adding a Button

Chapter 10 -- A Smarter Cart

E1 -- Creating a Smarter Cart

E2 -- Handing Errors

E3 -- Finishing the Cart

Chapter 11 -- Adding a Dash of Ajax

F1 -- Moving the Cart

F2 -- Creating an Ajax-Based Cart

F3 -- Highlighting Changes

F4 -- Hiding an Empty Cart

F5 -- Testing Ajax Changes

Chapter 12 -- Check Out!

G1 -- Capturing an Order

G2 -- Atom Feeds

G3 -- Pagination

Chapter 13 -- Sending Mail

H1 -- Sending Confirmation Emails

H2 -- Integration Testing of Applications

Chapter 14 -- Logging In

I1 -- Adding Users

I2 -- Authenticating Users

I3 -- Limiting Access

I4 -- Adding a Sidebar, More Administration

Chapter 14 -- Internationalization

J1 -- Selecting the Locale

J2 -- Translating the Storefront

J3 -- Translating Checkout

J4 -- Adding a Locale Switcher

Chapter 15 -- Deployment and Production

K1 -- Deploying with Phusion Passenger and MySQL

K2 -- Deploying Remotely with Capistrano

K3 -- Checking Up on a Deployed Application


Commits -- Observations and Detours Required

Initialization

rails new agile_web_dev_depot -d mysql

Iteration A1

rake db:create # create databases
rake db:migrate

Iteration A2

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.

Iterations B1 & B2

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

Iterations C1, C2, C3 & C4

No changes.

Iterations D1, D2 & D3

No changes.

Iterations E1, E2 & E3

No changes.

Iteration E Playtime

Getting price into line_items table seemed important enough... And changing LineItem model to use appropriate pricing.

Iterations F1, F2, F3, F4 & F5

No changes... F5 iteration was added to capture testing changes, rather than appending those to F4.

Iteration G1

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

Iteration G2

No changes.

Iteration G3

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

Iteration H1

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).

Iteration H2

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

Iterations I1, I2, I3 & I4

No changes.

Iteration I <3 Cargo Cult Security

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...

  1. Update gemfile to include required bcrypt-ruby gem

    gem "bcrypt-ruby"

  2. Install new bundle

    bundle install

  3. Restart server

Be sure to restart the application server.

  1. 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

    'password' is a virtual attribute

    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

  2. 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') %>
  1. 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).

  1. 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 %>
  1. 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.

Iteration J1

No changes.

Text should have explained removal of get "store/index" when scoping routes by locale.

Iteration J2

No changes.

Iteration J3

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&oacute;n"
      name:       "Nombre"
      email:      "E-mail"
      pay_type:   "Forma de pago"
  errors:
    messages:
      inclusion:  "no est&aacute; 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"

Iteration J4

No changes.

Iterations K1, K2 & K3

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.

About

Depot application exercise from "Agile Web Development with Rails"

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published