Bike's brake

Como contornar erros no brakeman: “User controlled method execution” e “Unsafe reflection method constantize called with parameter value”

Eu tenho dois models diferentes mas que ambos respondem aos mesmos métodos, no esquema duck typing, e preciso criar uma action show pra eles.

Porém, se eles respondem aos mesmos métodos, pra que vou criar 2 actions show em 2 controllers diferentes se elas serão exatamente iguais?

Desenvolvedor Ruby malandro que é, você cria uma rota que vai te passar uma referência ao model, além do ID, e me lança a seguinte action:

# routes.rb
get 'clients/:contactable/:id', to: 'clients#show', as: :contactable

# ClientsController
def show
  contactable_model = params[:contactable]
  @contactable = current_company.send(contactable_model).find(params[:id])
  ...
end

E aí vem o nosso querido Brakeman, sempre preocupado com as falhas de segurança da nossa aplicação, lança o erro: “User controlled method execution”!

Você relê o código que escreveu e na mesma hora vê o mole que deu!  Usando send com qualquer coisa que o usuário preencher!?

Como resolver isso? Você pensa em usar o constantize porque se o usuário tentar marretar qualquer outra coisa vai dar exception de constante não existe, easy:

def show
  contactable_model = params[:contactable].singularize.classify.constantize
  @contactable = contactable_model.where(company: current_company).find(params[:id])
end

Aí vem o Brakeman e lança: “Unsafe reflection method constantize called with parameter value”…

Você para um pouco e reflete sobre isso… verdade, ainda é muita informação que você tá dando pro possível atacante… como resolver então?

A forma que mais curti foi a sugerida pelo Paul Kwiatkowski:

class ClientsController < ApplicationController
  CONTACTABLE_MODELS = {
    'contacts' => Contact,
    'connections' => Connection
  }.freeze

  def show
    contactable_model = CONTACTABLE_MODELS.fetch(params[:contactable])
    @contactable = contactable_model.where(company: current_company).find(params[:id])
  end
end

Dessa forma, se o atacante tentar qualquer outro valor além dos que estão nas chaves da constante, uma exception será levantada de cara pelo fetch. 👍

fast red car

Dicas de performance pra Ruby: modifique as variáveis usando os métodos com bang!

Métodos com bang (!)

No Ruby temos as versões com bang (!) de vários métodos das classes principais, como: collect!, downcase!, flatten!, reverse! e capitalize!.

Essas variações dos métodos, na maioria das vezes, indica que você vai modificar o objeto ao invés de fazer uma cópia do objeto, altera-la e retorna-la.

Como economizar memória

Pela explicação você já deve ter percebido essa prática forma de economizar memória no Ruby: usar os métodos com bang sempre que possível!

Vamos exemplificar com código. Começaremos criando uma string com 50 megas:

huge_string = 'a' * 1024 * 1024 * 50

Agora, se usarmos o método upcase pra colocar todas as letras para maiúscula, o sistema precisará alocar mais 50 megas de memória só para fazer essa alteração:

huge_string = huge_string.upcase

Porém, usando a versão com bang, upcase!, essa enorme string não precisará ser replicada e evitaremos esse uso extra de memória!

Vale só para strings?

Nope! Isso serve pra tudo: hash, array, enumerable, etc…

A regra é simples: se não precisa copiar a variável, procure usar o método que faz a modificação in place ;D

Usando field type daterange com Rails e PostgreSQL

Outro dia precisei criar um model que permitisse ao freelancer marcar sua disponibilidade no Bonsai. A idéia é que ele pudesse marcar que está disponível full-time, part-time ou indisponível durante um certo período de dias.

Uma boa alternativa para a clássica combinação de campos “start_date” e “end_date” foi usar o field type daterange que o PostgreSQL possui e o Rails suporta muito bem.

Como defini-lo na migration:

create_table :availabilities do |t|
  t.daterange :period
  ...
end

Trabalhando com campos daterange

Filtrar os records com períodos que comecem a partir do início da atual semana:

scope :from_current_week_onward, -> { where("lower(period) >= :beginning_of_week", beginning_of_week: Date.current.beginning_of_week) }

Pesquisar por um período exato:

Availability.where("period && daterange(:start, :end)", start: Date.current, end: 3.days.from_now)

O campo tem todos os métodos de um Range do Ruby:

a.period # => Mon, 02 Jan 2017...Mon, 09 Jan 2017
a.period.begin # => Mon, 02 Jan 2017
a.period.end # => Mon, 09 Jan 2017

Por que eu uso Date.current? Leia esse post que escrevi no blog da HE:Labs: Evitando problemas com datas e timezones no Rails.

Gotcha

Atenção pra um detalhe importante: o Rails sempre salva o range usando a versão exclusiva dela:

availability.period = Date.current.beginning_of_week..Date.current.end_of_week # => Mon, 02 Jan 2017..Sun, 08 Jan 2017
availability.save # => true
availability.reload.period # => Mon, 02 Jan 2017...Mon, 09 Jan 2017

Isso é um saco, apanhei bastante até perceber isso então espero que esse post ajude a poupar o seu tempo 😁

Quer poder definir horários nos ranges também?

Se você precisar usar horas e minutos no seu range, também é simples, basta usar o field type tsrange ao invés do daterange. 😉

Ruby already has its own regular expression to validate emails

I’ve searched and written a lot of regular expressions to validate email on our models and forms but recently I’ve found out that Ruby already has a good one and its easy to access and use.

All you have to do require uri library and use its constant:

require 'uri'
'user@gmail.com'.match(URI::MailTo::EMAIL_REGEXP).present?

It’s also easy to use on an Active Record format validation:

validates :email, format: { with: URI::MailTo::EMAIL_REGEXP, message: "only allows valid emails" }

It even accepts emails with “+” as gmail enables.

 

Security - Private

Como private methods funcionam no Ruby

Uma confusão comum em Ruby é com métodos de classe privados. Veja o código a seguir:

class TwitterAccount
  def self.exibition_name
    "Cayo Medeiros #{username}"
  end

  private
    def self.username
      "yogodoshi"
    end
end 

Muitas pessoas acreditam que o método self.username está privado devido a declaração private logo antes dele. Porém, essa declaração só funciona para métodos de instância, não para os de classe.

Para transformar um método de classe em privado, utilize:

private_class_method :username

Para maiores detalhes, leia nesse post a resposta do criador do Ruby sobre o porque ter sido feito dessa forma.

Cuidados ao chamar models em migrations do Rails

Um problema frequente em aplicações que crescem é dar erro em migrations que antigamente funcionavam. Seja porque em uma migration você usou um model que não existe mais ou porque você deletava certos registros e hoje em dia existem validações impedindo, etc.

Prevenir o erro

Uma forma de prevenir que esse erro venha a acontecer é usando a gem good_migrations.

Ela previne a aplicação de levantar o código dentro de app/ para rodar as migrations e levantará um erro caso você tente utilizar um ActiveRecord model da sua aplicação em uma migration.

O jeito certo de fazer

Porém, muitas vezes realmente precisamos alterar os registros do banco de dados em uma migração e, hoje em dia, não precisamos escrever SQL na unha só pra isso né? #comofas então?

Crie um model bem simples dentro mesmo da migration, exemplo:

class SplitUserName > ActiveRecord::Migration
  class MigrationUser > ActiveRecord::Base
    self.table_name = :users
  end

  def up
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string

    MigrationUser.find_each do |u|
      u.update_columns(
        first_name: u.name.split(" ").first,
        last_name: u.name.split(" ").last,
      )
    end
    
    remove_column :users, :name
  end
end

How to solve rails 4 UndefinedTable Error when creating namespaced models

In a given rails 4 application, I have two namespaced models with a has_many association between them:

# models/review/asset_type.rb
module Review
  class AssetType < ActiveRecord::Base
    belongs_to :review_asset_category, class_name: Review::AssetCategory
  end
end

# models/review/asset_category.rb
module Review
  class AssetCategory < ActiveRecord::Base
    has_many :review_asset_types, class_name: Review::AssetType,
             foreign_key: 'review_asset_category_id'
  end
end

With migrations as:

class CreateReviewAssetCategories < ActiveRecord::Migration
  def change
    create_table :review_asset_categories do |t|
      t.string :name
      t.timestamps null: false
    end
  end
end

The migrations ran well but everytime I ran the tests I received the error:

Failure/Error: it { is_expected.to respond_to(:name) }
     ActiveRecord::StatementInvalid:
       PG::UndefinedTable: ERROR:  relation "asset_categories" does not exist
       LINE 5:                WHERE a.attrelid = '"asset_categories"'::regc...
                                                 ^
       :               SELECT a.attname, format_type(a.atttypid, a.atttypmod),
                            pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
                       FROM pg_attribute a LEFT JOIN pg_attrdef d
                         ON a.attrelid = d.adrelid AND a.attnum = d.adnum
                      WHERE a.attrelid = '"asset_categories"'::regclass
                        AND a.attnum > 0 AND NOT a.attisdropped
                      ORDER BY a.attnum

The solution is simple, just add this class method to each model or to the module that is namespacing the models:

module Review
  def self.table_name_prefix
    'review_'
  end
end

I hope this simple advice will save some time for other developers 🙂