testable constraint

関心

Rails を利用するに当たって先日の脆弱性

Rails 4.2.11.1, 5.0.7.2, 5.1.6.2, 5.2.2.1, and 6.0.0.beta3 have been released! | Riding Rails

の対策などはいちいち controller に処理が渡る前に処理を終わらせて 404 などのエラーを返してしまいたい。実際に稼働中のアプリに脆弱性がない場合でも悪意のある bot 対策という意味ではいろいろなケースで同様のことが言える。

これは具体的には routing の constraint を利用することで実現可能である。1

このような設計にしておくことには controller の before_action callback などで処理する手法に比べて以下のようなメリットがある。

  • action の dispatch に掛かるコストを節約できる(脆弱性目的のアクセスは過負荷になりがち)
  • controller は自分の責務に集中したコードだけを書けばよいので読み書きしやすい

ではどうやってこれを実現すればよいだろうか。

複雑なconstraintをProcで書くのは無理がある

しかし constraint を Proc で書こうとすると非常に書きにくい。単純な文字列比較などならよいが、request オブジェクトの中身を検証するようなものは

  • コードが長くなって rouitng が読みにくくなる
  • そもそもテストしにくい
    • routing のテストになるが、routing spec の DSL ではそのまま request header をセットすることができない
  • 条件を変えてテストコードを書いておきたいが、例えば constraint が外れていた場合に実際に問題が発生する(脆弱性に繋がる例外があがる)ことを検証しようとするとますます複雑になる

という問題がある。

Objectで書こう

constraint は Object と method で書くこともできる。

Rails Routing from the Outside In — Ruby on Rails Guides

これを読むと

#matches?

を呼べさえすればよい。#matches? に request が渡ってくるのでオッケーかどうかを true か false で返してあげるという寸法だ。

class Constraint
  def matches?(request)
  end
end

で new して渡しても

class Constraint
  def self.matches?(request)
  end
end

でそのまま渡しても include Singleton してもよい。

テストする場合は request object をそのまま渡せばよいので、ActionDispatch::Request や ActionDispatch::TestRequest を使えばよい。

これで routing spec の DSL が足りないといって気を揉む必要もなくなる。

条件を変える

constraint のテストだけでは constraint が与えた request に基づいて true を返すか false を返すかしか検証できない。慣れてしまえばこれだけで十分と判断できるかもしれないが、本当のことを言うと特定の条件を満たす request に対して Rails アプリがどのように反応するかもテストしたいはずである。つまり、constraint が有効な場合と無効な場合のテストが書けないと不安にならないだろうか?

constraint は基本的に routing にそのまま書くので適用されっぱなしになってしまう。Proc オブジェクトで書く場合は Proc の中で条件分岐させることができるが、Object と matches? メソッドで書く場合はそういうわけにいかない。

そこで stub out 可能なメソッドを用意しておく。こんな感じだ。

class Constraint
  def enabled?
    true
  end

  def matches?(request)
    if enabled?
      ..
    else
      true
    end
  end
end

これでテストの際には enabled? を false に stub out してあげれば、constraint が無効な場合にどんな問題が起きるかをテストできるようになった。そのうえで e2e の request を投げてあげれば、Rails アプリ全体がどのように反応するかが分かる。

余談だが、簡単に stub out できる Plain Old Object スバラシイと改めて思う。

  1. Rack middleware や Web サーバでも可能だけど response の返し方を揃えておくとか、利用しているフレームワークのそばに書いてある方が関連を理解しやすいという意味では Rails の中で処理するのが望ましそう。異論は認める。 

More

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Develop Document Jekyll