Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20200406前編)Ruby 2.7.1セキュリティ修正、RailsビューHTMLにテンプレート名を出力、Action Mailboxテスト用フォーム改良ほか

こんにちは、hachi8833です。オードリー・タン氏の次なる快挙です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

臨時ニュース: Ruby 2.7.1などがリリース(セキュリティ修正)

以下でお知らせした3月のセキュリティ修正↓とは別の修正です。

Rails 6/5とRubyのJSON gem向けセキュリティ修正がリリース


つっつきボイス:「RubyのJSONに別の修正が入ったそうです」「お、これはアップグレードすべき」「手元ではアップグレード完了しました😋」「Ruby 2.7〜2.4まで対象か😳」

Rails: 先週の改修(Rails公式ニュースより)

今回はコミットリストから見繕いました。

en_US.UTF-8環境を指定しない場合のバイトシーケンスエラーを修正

# guides/Rakefile#L14
- task :encoding do
-   %w(LANG LANGUAGE LC_ALL).each do |env_var|
-     ENV[env_var] = "en_US.UTF-8"
-   end
- end
-
  namespace :generate do
    desc "Generate HTML guides"
-   task html: :encoding do
+   task :html do
      ENV["WARNINGS"] = "1" # authors can't disable this
-     ruby "rails_guides.rb"
+     ruby "-Eutf-8:utf-8", "rails_guides.rb"
    end

    desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211"
-   task kindle: :encoding do
+   task :kindle do
      require "kindlerb"
      unless Kindlerb.kindlegen_available?
        abort "Please run `setupkindlerb` to install kindlegen"
      end
      unless `convert` =~ /convert/
        abort "Please install ImageMagick"
      end
      ENV["KINDLE"] = "1"
      Rake::Task["guides:generate:html"].invoke
    end
  end

つっつきボイス:「Railsガイドをジェネレートするrakeコマンドの修正か」「Rails本体ではないと」

「ははぁなるほど、ロケールにen_US.UTF-8がない環境で動かすと死ぬということね😆」「これって一般的でないロケールなんでしょうか?」「いえいえ一般的なロケールです☺️」

「Linuxシステムに入っているロケールはOSの環境に依存します: プルリクにもあるようにコマンドプロンプトでlocale -aと打つとロケールのリストが表示されるんですけど、そこにen_US.UTF-8が入っていなければ、それに依存するコードは当然死にます🧐」「なるほど!」「やってみたら出た出た😋」「ロケールに縛りかけられている環境ってたまにありますし☺️」

「逆にロケールに縛られないコードを書きたければロケールにCを指定すればOK」「LANG=Cはよく使います😋」「sshした先でコンソールに日本語を出してぐちゃら〜っと文字化けしたときなんかにexport LANG=Cしたり😆」「😆」「en_USなのにUTF-8を指定する必要ってあるのかという気持ちはワカル😆」

新機能: previously_new_record?が追加

# activerecord/lib/active_record/persistence.rb#L431
    # オブジェクトが作成されていたときにtrueを返す。ただしsaveする前で
    # オブジェクトがデータベースに存在せず、かつ`new_record?`がtrueを返した場合。
    def previously_new_record?
      sync_with_transaction_state if @transaction_state&.finalized?
      @previously_new_record
    end

つっつきボイス:「previously_changed?になぞらえたメソッド名にしたそうです」「おっとActive Recordのdirty関連ですね😆」「また増えた😆」「普通に考えるとpreviously_new_record?ってぱっと意味わかりにくいし😅」「直前がnew recordだった場合か🤔」「ちょっと微妙な響き😆」「こういうのが欲しいときがあるのか😅」

Active Recordモデルにpreviously_new_record?メソッドを追加した。これはモデルが新しいレコードで、最後に保存する前の状態の場合にtrueを返す。x_previously_changed?メソッドになぞらえてpreviously_new_record?という名前にした。

「今日は新メンバーもつっつき会にROM参加しているので、もしわからなければ"active record dirty"で検索してみるといいと思います😋」「ソースコードはだいぶ泥臭いと思いますけど😆」「たまにバグってたりしますし😆」「dirty周りの直しって割とよくありますね」「シンプルに使えばいいと思うんですけど、極まった使い方をする人がいたりしますし😆」

参考: 1.4 Dirtyモジュール -- Active Model の基礎 - Railsガイド

Base64のstrict_decode64urlsafe_decode64に変更

# actionpack/lib/action_controller/metal/request_forgery_protection.rb#L394
      def real_csrf_token(session) # :doc:
-       session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
-       Base64.strict_decode64(session[:_csrf_token])
+       session[:_csrf_token] ||= SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
+       Base64.urlsafe_decode64(session[:_csrf_token])
      end

Base64 strict-encoded CSRFトークンはwebsafeを継承していないので扱いが難しかった。たとえばCSRFトークンをクライアントリーダブルなcookieでブラウザに送信するのによく使われる方法が簡単には使えない。無事に送信するには値がurl-encodedかつurl-decodedでなければならない。
今回Base64 urlsafe-encoded CSRFトークンを生成するようにした。これは送信への継承が安全である。後方互換性のためバリデーションはurlsafeトークンとstrict-encodedトークンの両方を受け取れる。
同コミットより大意


つっつきボイス:「strict-encodedなCSRFトークンってそもそも何だろう?🤔」「そういえばBase64そのものは特殊文字も含めてエンコードできるんだった」

strict_encodedstrict_decode64のRuby APIを見ると『このメソッドは [RFC4648] に対応しています』とある」「そしてurlsafe_encode64urlsafe_decode64は、URLでも使われる+/を別の文字に置き換えてエンコードするのね😳」

このメソッドは [RFC4648] の "Base 64 Encoding with URL and Filename Safe Alphabet" に対応しています。 "+" を "-" に "/" を "_" に置き換えます。
同APIより

「そしてRFC 4684の5章を見ると『ファイル名セーフなアルファベット』にするとある、ははぁこれか!」「ファイル名セーフにしたいときはたしかにある」「~なんかも置き換えられると」「おぉ」「RFCで定義されてるなら大丈夫そう😋」

参考: RFC 4648 - The Base16, Base32, and Base64 Data Encodings


RFC 4648より

さまざまなBase-nエンコーディング

「どうやらBase32なんてのもあるらしいし😆」「おほ、数字の1とかがないのが面白い↓」「不思議😳」「何か意図があるんでしょうね〜」「アルファベット大文字のアイIと紛らわしくないようにとか?」「それあるかも!」「ゼロ0がないのも大文字のオーOと紛らわしくならないようにということだったりして🤔」「よくそんなの気づきますね👍」「いえいえ😅」「言われてみればプリンタブル&ビューマンリーダブルなエンコードっぽい」


RFC 4648より

「Base16もある〜😆」「これはそのまんま16進数か😆」「さすがにBase8はない😆」「プルリクと関係ない話になったけど学びある🧐」「ともあれCSRFトークンにurlsafeを使うべきというのもごもっとも👍」


RFC 4648より

新機能: Action Mailboxで受信メールをフォームのsourceに貼ってテスト配信

# actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails/sources_controller.rb#L3
+module Rails
+ class Conductor::ActionMailbox::InboundEmails::SourcesController < Rails::Conductor::BaseController
+   def new
+   end
+
+   def create
+     inbound_email = ActionMailbox::InboundEmail.create_and_extract_message_id! params[:source]
+     redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
+   end
+ end
+end

emlファイルを貼り付けてテストするのに便利。
同PRより大意

DHH自らのプルリクです。


つっつきボイス:「テストでうれしい改良みたいですけど、sourceっていうのが何を指すんだろうと思って」「Action Mailboxみたい」

「あ〜そういうことか!: Action Mailboxのデフォルトのジェネレータで生成されるフォームにこれがあるんですよ」「おぉ?」「Action Mailboxはメールの受信をトリガとして動くんですけど、ローカルテスト用に受信メール送信フォームがscaffoldで作られます」「ふむふむ」「これはRailsの普通のscaffoldと同じようなフォームになっていて、Newを押してsubjectとbodyを入力できるようになっているんですけど、このsourceというのはいわゆるメールのemlファイルをそこにべたっと貼り付けられるようになっているということですね🧐」「あ〜そういうことですか!😳」「コミットメッセージに書いてあったemlってタイポかと思ったらファイル形式でしたか😅」

「じゃテストでうれしいというのは...」「今までのAction Mailboxのフォームだと、たとえば添付ファイルが複数あるメールとか、HTMLメールとプレーンテキストメールの組み合わせとかを送信できなかったんですよ: そういうのはMultipart形式でboundaryとかを書かないといけないので」「はぁ〜なるほど」「そういうメールを生でフォームにコピペできるようにしたということですね😋」「これはたしかに欲しい😋」「任意の添付ファイルチェックみたいに、これがないとテストできないケースはたしかにありますね☺️」

以下は銀座RailsでのAction Mailbox解説スライドです(発表: morimorihoge

Active SupportのRange#cover?の挙動をRubyに合わせて修正した

# activesupport/lib/active_support/core_ext/range/compare_range.rb#L66
    def cover?(value)
      if value.is_a?(::Range)
+       is_backwards_op = value.exclude_end? ? :>= : :>
+       return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
        # 1...10 covers 1..9 but it does not cover 1..10.
        # 1..10 covers 1...11 but it does not cover 1...12.
        operator = exclude_end? && !value.exclude_end? ? :< : :<=
        value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
        super(value.first) && (self.end.nil? || value_max.send(operator, last))
      else
        super
      end
    end

つっつきボイス:「Rubyのcover?とActive Supportのcover?の挙動が違ってたのを修正したそうです」「cover(6..3)みたいに逆順に書けるって知りませんでした😳」「そんな書き方したことない〜😆」「手元でirbしてみるとたしかに逆順で書ける!」「こういう書き方がありだったとは😆」「よくぞ見つけた😆」「===include?も同様に更新されてますね😋」

# Rubyの場合
(1..10).cover?(6..3) #=> false

# Railsコンソール(Active Suport)の場合
(1..10).cover?(6..3) #=> true

ビューのHTMLにテンプレート名を含められるようにした


同PRより


つっつきボイス:「ビューにテンプレート名を表示?」「おぉぉなるほど、こういうコンフィグ↓でテンプレート名を表示できるのか、これいいじゃん❤️❤️」「おお喜びに満ちてる🥰」

config.action_view.annotate_template_file_namesを設定すると、各テンプレートの開始位置と終了位置を示すHTMLコメントがレンダリング出力に追加される。@jhawthornが#35407で導入したステキなActionView::Template#short_identifierが使われる。
(メモ: ChromeはHTMLタグの外にあるHTMLコメントをbodyの内側に移動する。<!-- END app/views/layouts/application.html.erb -->が本来ドキュメント全体の閉じタグであるにもかかわらずbodyの内側にある理由はこれ。)
本PRは@tenderloveとの共作。
同PRより抜粋・大意

「どこからどこまでがテンプレートなのかわからなくなるので入れたんですね」「これはデバッグ用にとっても欲しい😘」「今までは自分でテンプレート名を入力してたりしましたし」「自分で書いたビューならまだしも、他人の書いたビューのコードでパーシャルが大量に使われていたりするとつらいんですよ😢」「とくに複数のパーシャルの出力が互いにとってもよく似てたりするとなおさら😭」

「さらに厄介なのはパーシャルのパスの解決順序: 基本的には今いるディレクトリから近いところを探すんですけど、ない場合は親ディレクトリをたどったりするので、そこにあるパーシャルの出力がほとんど同じだったりすると、今ここで出力されているのはいったいどっちのパーシャルなんだ?ということになったりしますし😡」「あああ😅」「こういうことが割と普通によく起きるんですよホント😇」「ビューのこういう調査ってしんどい😭」

「とにかくこれはいい機能👍👍」「ローカルのdevelopmentだけで使うんですよね?」「もちろんローカルとかstagingとか限定で☺️」

番外: 存在しないenumがクエリにある場合にエラーを出すようにした(revertされました)

アプリにこの挙動を入れるとびっくりさせすぎるので取り消した。デフォルトでraiseするより先に振る舞いを非推奨化する必要がある。
@f3dfed7より

# @f3dfed7より
# activerecord/lib/active_record/enum.rb#L142
      def serialize(value)
-       assert_valid_value(value)
        mapping.fetch(value, value)
      end

つっつきボイス:「存在しないenumをクエリで使った場合のエラーか」「びっくりさせすぎるということでコミットが取り消されてました」

データベースのenumは考えてから使おう

「ちなみに自分はenum好きじゃないのであんまり使わない😆」「😆」「もちろん好みはあると思うんですけど、enum使うとデータベースの出力がわかりにくくなるんですよ😅」

参考: 8.7. 列挙型 -- PostgreSQL 11

「自分はバグ修正とかするときには、Railsコンソールでやるよりも昔ながらのSQLを最終的に書きたいタイプなんですけど、enumを使われると、値の意味を知るのに毎回ソースコードを見にいかないといけなくなる😭」

参考: RailsのEnum: 諸刃の剣(1) from Viblo | Framgia Journal

「まあ最近のRailsだとenumを参照すると一応Rails側のキーの文字列で取れるんですけど」「ひどいときはテーブルAでは0がactiveなのにテーブルBでは1がactive、なんていうプロジェクトがあったり😭」「あぁ...😅」「そういうプロジェクトで障害調査のためにSQLを書くと結果のenumのところでマジわからなくなりますし😤」「お察しします😅」「enumを使われるとSQLで調査したいときにSQL出力がリーダブルにならないのがホント困る😢」

参考: Rails5でenum定義したカラムの元の値を取得 - Qiita
参考: ActiveRecordのEnumの取り扱い方 - Qiita

「まあデータベースの管理権限があれば、最悪の場合enumを展開したデータベースビューを定義してそっちをSELECTする手が使えるんですけど、それもできないときはCASEとWHENをひたすら使ってenumを文字列に戻す神SQLを書いたりしますし」(一同ソースを見て)「うぅ、これはつらそう😅」「こうしないとヒューマンリーダブルにならないのってつらすぎるし」「戻すときの対応関係を間違えたりするとさらに地獄👿」「正直、よほどでかくない限りenumにしても大して速くなりませんし😆」「😆」

Rails

DID.appでパスワードレス認証(RubyFlowより)


did.appより


つっつきボイス:「このdid.appというのがパスワードレス認証サービスをやってるそうです」「こういうふうに↓認証部分をdid.appにリダイレクトできるということか」「あとはルーティングにちょっと手を入れてコントローラにコールバックを追加すればいいと」「Railsでこうやれば私たちのサービスで認証できますよと」「OmniAuthとかに近そう😋」

<!-- 同記事より -->
<form action="https://auth.did.app/oidc/authorize" method="get">
  <input name="client_id" value="<%= ENV["CLIENT_ID"] %>" type="hidden" />
  <input name="redirect_uri" value="<%= callback_url %>" type="hidden" />

  <button type="submit">Sign in</button>
</form>
# 同記事より
require 'faraday'
require 'json'

class SessionController < ApplicationController
  def callback
    response = Faraday.post(
      "https://auth.did.app/oidc/token",
      client_id: ENV["CLIENT_ID"],
      client_secret: ENV["CLIENT_SECRET"],
      code: params["code"]
    )
    data = JSON.parse(response.body)

    session[:current_user_id] = data["userinfo"]["sub"]
    redirect_to root_path
  end
end

「いわゆるCRM(Customer Relationship Management)系のサービスなんでしょうね☺️」「ユーザー管理以外にもいろいろやってくれるんでしょうきっと」「1000ユーザーまでは無料か🤔」「なるほどそういうサービスですか」

参考: CRMとは何ですか? ~メリットデメリット & 活用と運用のコツ~

「こういう部分の実装は面倒なので、外に出したい気持ちはありますね」「たしかに」「1つのアプリだけメンテするならともかく、自分でホスティングしている認証サービスを他のアプリでも共用するようになってくると、その認証サービスが落ちた瞬間に他のアプリが全部動かなくなりますし」「あ、そうか😳」「そういう状態になってしまうと、計画メンテがとてもやりにくくなってしまう😅」「ですね...」

「結局システム間依存が増えれば増えるほど容易に止められなくなってしまうんですよ: たとえばあるシステムは夜中ならほぼ誰も使っていないからと思って夜中にデカくて重いバッチを走らせてみたら、そこに依存していた他のシステムが止まったなんてことになりかねませんし😇」「う〜む」「そういう依存度を下げるためにこういうサービスを使うのはひとつの方法になると思います☺️」

「まあGoogle認証とかOAuth 2とかでもいいと思いますけど😆」「自分もそれにしようかな...」「ただそういうサービスは認証しかやらないので、計測みたいな他の便利な機能はありません🧐」「あ、そうか」「まあ単なる認証と目的が違うので: このdid.appがやっているようなカスタムブランディングとかアクティブユーザーインサイトみたいな情報はOAuth 2とかだけでは取れませんし」「そういうのがやりたいときにはいいんじゃないでしょうか☺️」

参考: Google 2 段階認証プロセス
参考: OAuth 2.0 — OAuth

Railsでマルチステップのフォームを作る方法(Ruby Weeklyより)


つっつきボイス:「いわゆるウィザード的なフォームをRailsでやる記事です」「こういうマルチステップフォームってRailsのCRUDと相性が悪いんでしたっけ?」「まあマルチステップフォームは既にCRUDじゃないので😆」

「こういうのはForm Objectを作ればだいたいやれますけど」「そういえば今日はいませんけど、kazzさんが以前こういうフォームは面倒じゃ〜って言いながら作ってました」「マルチステップのフォームは、フロントエンドでやるのかバックエンドでやるのかを決めるのも含めて、いろいろ面倒😆」「ですね😆」

「マルチステップのフォームをフロントエンドでやるならRails側は基本的にCRUDでいいんですけど、Rails側も多少マルチステップを意識した実装にしないと親切なフォームを作るのは難しいんですよ😅」「うーむ」「たとえばマルチステップの第一段階でサーバーサイドでのバリデーションが必要なチェックが入ると、結局ステップ1用のバリデーションを用意しないといけなくなりますし」「なるほど」「よくあるのが、ユーザーアカウントを作成するときのアカウント重複チェックなんですけど、そういうふうに項目がまだ全部入力されてないけどバリデーションしたいことも多いですし」「バリデーションを個別のAPIにバラしてフォーム送信前にチェックする方法も考えられますけど、今度は複数項目を組み合わせてバリデーションしたくなることもありますし😆」

「マルチステップフォームはどうやっても面倒になりますし、結局この面倒臭さをフロントとバックエンドのどちらで巻き取るかでしょうね☺️」「どちらかに寄せるのがいいと思います」


記事要点:

  • 各ステップが単独のActive Recordモデルにだけ関連する分にはやりやすい
  • 各ステップとモデルが1対1対応しないと面倒になる
  • wicked gemを使う手もあるが、自分は外部ライブラリに依存しない方法を選んだ
    • マルチステップフォームは案件に固有なものが多いのでgemがうまくはまるとは限らないと思った
  • session storageに保存しておいて最後にまとめる方法は必ずしも必要とは限らない
  • 大事なのは、マルチステップフォームをわかりやすいコードで書いてメンテ可能に保つこと
    • 名前空間を使うなど

Railsのセキュリティベストプラクティスと脆弱性リスト、そしてRails脆弱性の賞金サイト


同記事より


つっつきボイス:「今回はこれも含めて長い記事が多くなりました😅」「なるほど、こういう賞金を稼げるサイト↓も紹介されてますね😋」「おぉ💰」「Railsのクリティカルな脆弱性を見つけると15万円もらえる😆」「😆」「😆」


hackerone.com/railsより

「こうやってセキュリティに賞金をかけるのは最近一般的になりましたね」「インセンティブで釣るのが大事😋」「賞金だけで食べてる人はそんなに多くはないようですけど、実際にいるみたいですヨ❤️」「こういうのはだいたいドルベースなので、物価の安い国なら一発当てるとしばらく暮らせたりするようですし🏝」「むふふ😋」「日本でもこれで生活してるっぽい人が何人かいるようですし、まあどこかに所属してる人も多いんでしょうけど😆」

「それこそ今話題のZoomも賞金かけるんじゃ?😆」「😆」「例のZoomの声明↓でも、たしかその辺の対応を始めるって載ってたと思います☺️」「賞金を拡充するってありますね」「HackerOneにはまだZoomはないっぽいですけど😆」

参考: A Message to Our Users - Zoom Blog

「まあRailsの場合はとりあえずセキュリティ系のgemを入れておくことでしょう☺️」「あとRailsセキュリティガイドも読んでおくと」「こういうのは世の中でよいとされているものを入れておくのが好き🥰」「こういうドキュメントを読み込むのはとても勉強になりますね👍」

参考: Rails セキュリティガイド - Railsガイド

なおOWASP.orgのRailsのセキュリティ項目はなぜかなくなっていました↓。

参考: 404 - Not Found | OWASP -- リンク切れ

時刻を丸めるときのちょっとしたテクニック(Hacklinesより)


つっつきボイス:「すごく小ネタなのでコードは引用しませんが、Railsで時刻と時刻の差を丸めたいときにActive SupportのTime#changeでやれば割り算しなくていいよという記事でした」「なるほど😋」「これは使ったことなかったな〜」

参考: Time の一部を自由自在に改変したい - Qiita

その他Rails


つっつきボイス:「今年のRailsConfは、Couch Editionという名称でオンライン開催を目指しているそうです」「おおDHHにGitHubのUchitelleとAaron Patterson、錚々たるメンツ👍」「オンライン開催になったことで日本人としてはむしろ参加しやすくなった感ありますね🥰」「もちろん現地に飛んで会うからこそ得られるものもありますけど😆」「そこですよね...😅」


前編は以上です。

おたより発掘

バックナンバー(2020年度第1四半期)

週刊Railsウォッチ(20200317後編)Strangler Figパターンでリファクタリング、ペアプロ実践記事、イミュータブルデータモデルほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。