Wednesday 24 October 2007

Keeping passwords out of your logs

By default, rails will insert all parameters into the logs. This is useful for debugging, but not so grand when the parameters are sensitive eg:
Parameters: {"user"=>{"login => "mylogin", "answer"=>"my answer", "question"=>"my question ", "terms"=>"1", "new_password"=>"mypass123", "new_password_confirmation"=>"mypass123"}, commit => "Activate my account", "_method"=>"put"} ...
OR
Parameters: {"commit"=>"Log in", "action"=>"create", "controller"=>"sessions", "password"=>"mypass123", "login"=>"mylogin"} ...

It's a one-liner for rails to filter this out. Just add this line to the top of the relevant controller(s):

class SessionsController < ApplicationController
  # filter out sensitive fields from the log
  filter_parameter_logging 'password'
  # rest of controller goes here...
end
class UsersController < ApplicationController
  # filter out sensitive fields from the log
  filter_parameter_logging 'password', 'question', 'answer', 'given_answer'
  # rest of controller goes here...
end

This will turn the required log lines into:
Parameters: {"user"=>{"login => "mylogin", "answer"=>"[FILTERED]", "question"=>"[FILTERED] ", "terms"=>"1", "new_password"=>"[FILTERED]", "new_password_confirmation"=>"[FILTERED]"}, commit => "Activate my account", "_method"=>"put"} ...
OR
Parameters: {"commit"=>"Log in", "action"=>"create", "controller"=>"sessions", "password"=>"[FILTERED]", "login"=>"mylogin"} ...

Note that it's even smart enough to automatically filter out the "password_confirmation" field without requiring a specific reference to it.

Tuesday 23 October 2007

Extending ActiveRecord::Validations for positive numbers

I noticed I could DRY up a lot of my def validate items by finding a better way to check if a numerical field was a positive number (when provided). I figured this would be a perfect candidate for extending the "validates_numericality_of" method. But then I had a play with extending it and got stuck on "how exactly do you extend a class method?"

I've successfully added new validations by reopening ActiveRecord::Base, but how do you open an exising method and add more bits on?

I tried alising the method... but the question became: how do you alias a method that is defined as self.validates_whatever?

This excellent post describes how to extend class methods by using base.class_eval and putting the alises into class << self

Note: you definitely need to double-alias your function (as in the class extension section below) or you will find yourself instantly spiralling into a "stack level too deep" exception the first time you call it.

Here's how to extend a validation in Rails (includes my extension to validates_numericality_of, and also my preivous validates_percentage method):

module MyNewValidations
  # magic to allow us to override existing validations
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      class << self
        alias old_numericality_of :validates_numericality_of unless method_defined?(:old_numericality_of)
        alias validates_numericality_of :my_validates_numericality
      end
    end
  end

  module ClassMethods
    # extends the "validates numericality of" validation to allow the option
    # ":positive_only => true" or ":negative_only => true"
    # This will validate to true only if the given number is positive (>= 0)
    # or negative (<= 0) respectively
    # Otherwise is behaves exactly as the standard validation
    def my_validates_numericality(fields, args = {})
      ret = old_numericality_of(fields, args) # first call standard numericality

      pos = args[:positive_only] || false
      neg = args[:negative_only] || false

      if pos || neg
        msg = args[:message] || "should be a #{pos ? 'positive' : 'negative'} number"
        validates_each fields do |model, attr, val|
          if (pos && val.to_f < 0) || (neg && val.to_f > 0)
            model.errors.add attr, msg
            ret = false
          end
        end
      end

      ret
    end

    # validates whether the given object is a percentage
    # Can also take optional args which will get passed verbatim into the
    # validation methods. Thus it's only really safe to use ":allow_nil" and
    # ":message"
    def validates_percentage(fields, args = {})
      msg = args[:message] || "should be a percentage (0-100)"
      validates_each fields do |model, attr, val|
         pre_val = model.send("#{attr}_before_type_cast".to_sym)
         unless val.nil? || !pre_val.is_a?(String) || pre_val =~ /^[-+]?\d+(\.\d*)?%?$/
           model.errors.add(attr, msg)
         end
       end
      args[:message] = msg
      args[:in] = 0..100
      validates_inclusion_of fields, args
    end

  end
end

class ActiveRecord::Base
  # add in the extra validations created above
  include MyNewValidations

  #other stuff I'd defined goes here
end