Rails アンチパターン - 使われない権限メソッド(Authorization Astronaut)

はじめに

最近Rails AntiPatternsという本を読んでいました。

https://www.amazon.co.jp/dp/B004C04QE0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

幾つかの項目を読んだ所なかなかおもしろそうだったので一冊通して読もうと思っています。
翻訳版はないので、ざっくり訳しつつ自分の感想を書いていこうと思います。 2010年に発売されたもので、サンプルコードやgem等は古いので適宜読み替える必要があります。
また、サンプルコードは一部変更して掲載しています。

Authorization Astronaut

権限ロジックは仕様に基づいて、または将来の要件を予期してプログラムされます。その結果、一般的なユーザー認証では、次のようなユーザー・モデルが作られます。

class User < ApplicationRecord
  has_many :user_roles
  has_many :roles, through: :user_roles

  def has_role?(role)
    roles.exists?(name: role)
  end

  def can_post?
    has_roles?(%w(admin editor associate_editor research_writer))
  end

  def can_review_posts?
    has_roles?(%w(admin editor associate_editor))
  end

  def can_edit_content?
    has_roles?(%w(admin editor associate_editor))
  end

  def can_edit_post?(post)
    self == post.user || has_roles?(%w(admin editor associate_editor))
  end
end

can_*というメソッドは破滅への道です。いつ使われるかわからず、インタフェースも曖昧で一貫性がないです。今後も必要になる前から権限ロジックだけ先に追加されるようになります。 また、admin, editor等の権限名がハードコーディングされていて、変更に弱い状態です。

自分もこんな感じのをモデル見たことがあり、ああーなるほどという感じでした。モデルとコントローラを別の人で開発した場合にこんな感じになりがちな印象です。
ハードコーディングやめて定数にすればという人もいますが、本質的には変わらないと思います笑

ユーザにはロールをつけることができます。

class Role < ApplicationRecord
  has_many :user_roles
  has_many :users, through: :user_roles

  validates :name, uniqueness: true, presence: true

  def name=(value)
    write_attribute(:name, value.downcase)
  end

  def self.[](name)
    find_by(name: name.to_s)
  end

  def add_user(user)
    users << user
  end

  def delete_user(user)
    users.delete(user)
  end
end

Roleモデルにはいくつかの問題があります。管理ユーザーがロールを追加または削除できるようにする計画はありません。 また、Role.[]というメソッドは識別子の変更を吸収するのに使われるかもしれませんが、その意図で書かれていないため、機能しません。どこでも使われない可能性もあります。
add_user、delete_userも良いインタフェースではありません。

あるあるだw

要するに、これら2つのモデルは独立して書かれました。アプリケーションの仕様が固まる前に、将来何が必要になるかを見越して書かれています。これは認証ロジック実装でかなり発生するようです。仕様を固めている間に、認証ロジックが開発者が事前に開始できるものとして認識されるためです。
これは誤った仮定で、過剰に設計されたコードにつながり、あいまいで矛盾したインターフェイスを提供し、正しく使用されないか、まったく使用されなくなってしまいます。

とはいえ、チームの文化とかいろんな要因で、やりたいことを理解する前(仕様が固まる前とは違う)にロジック部分だけ作ってしまい、こうゆうコードが生まれるのはたまに見かけます。

Solution: Simplify with Simple Flags

前のセクションで説明した複雑さの問題に対処するために、次のようにモデルをリファクタリングすることができます。

class User < ApplicationRecord
end
class AddColumnToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin,  :boolean, null: false, default: false
    add_column :users, :editor, :boolean, null: false, default: false
    add_column :users, :writer, :boolean, null: false, default: false
  end
end

https://github.com/waterlow/authorization_astronaut/pull/2

これによってRoleモデルが削除でき、Userモデルにはadmin?editor?などのメソッドが使えるようになります。フレームワークの機能をを効果的につかい、「コードが無い」という一番きれいな状態になります。今後1つ2つのロール追加くらいならばこの方法でも問題は無いでしょう。

なかなか衝撃的でした。3つあったテーブルを一つにしてしまって、しかもUserモデルには何も書かないというシンプルさ…。仕事だとこう言う選択は非常に勇気が要りますが、シンプルさを保つ事ができる非常に良い方法だと感じました。

それ以上にロールが追加される場合は、テーブルを増やしましょう。ただし、多対多用の中間テーブルは作らず、has_manyで完結させます。

class User < ApplicationRecord
  has_many :roles

  Role::TYPES.each do |role_type|
    define_method("#{role_type}?") do
      roles.exists?(name: role_type)
    end
  end
end
class Role < ApplicationRecord
  TYPES = %w(admin editor writer guest)
  validates :name, inclusion: { in: TYPES }
end

https://github.com/waterlow/authorization_astronaut/pull/3

これは、不必要なコードを排除し、過剰に設計されていないため、ユーザーの役割を扱う方法について疑問のない一貫したインターフェイスを提供するため、成功しています。

やっぱり前者のadmin?メソッドにこだわっていて、Railsの機能が提供してくれるものが一番シンプルだという考え方ですかね。私もこの辺は同意です。自前で書くよりはよっぽどいい。あと、DBの外部キー制約が使えなさそうだけど、モデルでバリデーション入れてるからいいよねという気持ちだろうと思います。

ここで概説した概念は、ユーザーの役割にのみ適用されるものではありません。また、アプリケーションドメインをモデル化し、モデルによって提供されるインタフェースを定義するときに、他の多くの状況にも適用できます。以下の簡単なガイドラインは、オーバーエンジニアリングを止め、定義されていない仕様とアプリケーションの変更の両方に直面する簡単なインターフェイスを提供するのに役立ちます。

•コードを書くときにアプリケーションの要件を超えて構築しないでください。
•具体的な要件がない場合は、コードを記述しないでください。
•すぐにモデルを作ろうとしないでください。付加的なモデルの追加を避けるために、ブーリアンや非正規化などの単純な方法がよく使用されます。
•データの追加、削除、または管理のためのユーザーインターフェイスがない場合、モデルは必要ありません。 ハッシュまたは可能な値の配列で作成された非正規化列で問題ありません。

最後のはシリアライズのことをいっているのかなと思います。UIがない場合はモデル作らなくてもシリアライズで問題ないという考えは、以前ActiveRecord serialize / store の甘い誘惑を断ち切ろうという記事を読んで以来やらないほうがいいと思っていましたが、認可のようなアプリのメイン機能とは異なる場面では使ってもいいかなと思いました。
また、UIがない場合にモデルを追加しないという決断は驚きでした。

個人的な意見としては、空のモデルを作ってもいいかなとは思ったのですが、モデルを作ってしまう時点で将来の要求とはずれてしまうかもしれませんし、やはりモデルを作らないで済むのが一番拡張性の高い状態かなと思いました。

まとめ

仕事でこうゆうリファクタリングをぶっこんでいくのはなかなか難しそうですが、必要になるまで作らないという視点は常に持っておきたいです。

【LT】表参道.rbでActiveRecordのテーブル名の決め方等について発表してきました。

はじめに

ActiveJobのキュー名の設定をどうするかを考えていた過程で、ActiveRecordのテーブル名の決め方を調べていました。色々と面白いところがあったので表参道.rbで発表してきました。(ただのテーブル名あてゲーです。)

基本はシンプルに「親のテーブル名を見に行く」だけだった

f:id:waterlow2013:20170408221729p:plain

こんな風にabstract_classが複数入っていると難しく見えがちですが、AdminUserが読まれたときにテーブル名がadmin_usersになり、そのサブクラスのtable_nameは、指定しない場合はすべてadmin_usersとなります。
abstract_classはレコードを作れないということだけで、テーブル名の決め方には関係ありませんでした。

まとめ

普段深く知らないまま使っている機能について、ソースを読んで理解を固めていくのは非常にいい感じでした。

【LT】encrypted secretsについて社内LT大会でしゃべってきました。

はじめに

Rails5.1で導入されたencrypted secretsについて所属する会社の社内LT大会で発表してきました。

内容

5分だったので大したことは書いていませんが、機能の説明以外はstaging問題やメインです。

所感(使えそう?)

  • KMSをつかうならyaml_vaultかな
  • Railsのレールに乗っている感じが良い
  • 暗号化はガンガン修正が入らない前提。修正がたくさん入る場合は別の策を考える。

staging問題について

詳細

  • デフォルトではkeyと暗号化ymlは一組しか作られない。
  • Rails.env == 'staging' # => trueでも本番と同じkeyを使うことになる。
  • ステージングのkeyが流出するだけで本番のkey流出となってしまう。

解決策

  • secrets.#{Rails.env}.yml.encとそれに対応するkeyを別で持っておく。
  • 例えばsecrets.staging.yml.enc, secrets.production.yml.enc(rails secrets:setup, rails secrets:editしたらリネームする)
  • デプロイ時にln -sする。

まとめ

  • なにはともあれ設定ファイルコミットできるのは良い!

rebuild.fmのエピソード検索ができるwebサイトを作った

はじめに

Rails5.1の新機能を試すために、簡単なRailsアプリを作りました。 rebuild.fmのゲストとshownoteにフィルタリングをかけて検索できるようにしたものです。

heroku: https://shielded-river-53880.herokuapp.com/
idcf: http://rebuildfm-search.waterlow.tk/

https://i.gyazo.com/aa86202d4b40af933e4c06234c8fc3f1.gif

リポジトリ以下です。
github: https://github.com/waterlow/rebuild-search

使っている技術等

  • Rails5.1.0.beta1
  • encrypted secret
  • webpacker
  • itamae (vpcのプロビジョニング)
  • mysql

とにかく今回はRails5.1を使ってみるのが目的でした。encrypted secretは、普通に本番運用でも使える物だと思いました。ただherokuでは必要なさそうなのと、本番とドッグフーディング環境等でconfig/secrets.yml.keyを分けたい場合に少し考えないといけません。
そちらへの解も考えていたので、今度個別の記事にします。

webpackerには深入りしてはいませんが、問題なく動いていそう…。 compression-webpack-pluginで圧縮した結果、chromeでエラーが起こっていたので圧縮しないようにしました。 https://github.com/waterlow/rebuild-search/commit/e34c52569b52dbdb88945ddaa16a5e621cbfccd9

herokuでもvpcでも共通で、かつスタンダードなスタックにしたかったのでpostgresqlを考えていましたが、centos&itamaeでのインストールが上手く行かずに断念してしまいました。ただ、今後移行する予定です。

vpsでサービス立ち上げ、デプロイ等までちゃんとやったことがなかったのでいい勉強になりました!

今後

テスト、ci、自動デプロイを整備して、ストレスなく開発できるようにする。

【LT】Railsのyaml、巨大になる前になんとかしようという内容で発表してきました!

はじめに

最近しごとで巨大なyamlを何とかするという仕事をやっていて、いろいろ思うところがありました。
これは今後の糧にしよう!とおもい、スタートアップRails勉強会で発表してきました。

反応


会話など

  • そもそもmodelが多過ぎる
  • ちゃんとバージョンアップできているのはすごい
  • Railsアプリのコードをきれいに保つにはどうするか

 - 啓蒙や教育を怠らない
 - 機能を絞る(コアドメインで勝負する)

全体通して

抱えていたyaml肥大化問題は、「環境毎に値変えたい」と「秘匿情報でコミットしたくない」と「yamlに書かなくていいことまで書いてる」の3つがあるのかなと気づきました。発表に向けていろいろ整理できたのは良かったです。今後、それぞれに解決策を用意していこうと思います。

Qiitaに初投稿!「hamlでタグを改行しない方法の整理」という記事を書きました

はじめに

タイトルの通り、Qiitaデビューした。前々から投稿したいと思っていたのだが、「ネタがない」「時間がない」などの言い訳をしつつ渋っていましたが、この度初投稿しました。

なんでこの記事を書いたのか

以前働いていたところでは、デザインはデザイナーがhtml, cssまで書き、エンジニアはhamlで実装しているような形でした。 そこではしばしば、デザイナーの書いたhtmlと、hamlで書いて出力されるhtmlが微妙に違うということがしばしばありました。 その原因の1つが、hamlの強制改行によるものでした。

Railsエンジニア1年目の自分は、そもそもhtmlを直接書かないこと、hamlというものがあること、インデントがちょっとずれるだけでエラーでhtmlをrenderできないことなど四苦八苦。改行を操作する方法があるということにたどり着いたのはだいぶ後でした。

そこで、「haml 改行しない」でぐぐると、シンプルな記事がでてくるようになるといいなという思いからこの記事を書きました。

「ワニが改行を食べる」イメージ

答えはいたってシンプルで、> or <の「口が開いている方の改行」を消すという覚え方が良さそうです。 出典は公式ドキュメントです

まとめ

Qiita初投稿を終えて、だいぶハードルが低くなったので、今後も投稿していく

bootstrapとsimple_formのチュートリアル

目的

rails newしてから、bootstrapで管理画面を作るのに一番早い方法が何かを探っていた。
rails_adminやactive_admin等もあるが、今回はある程度自前カスタマイズを想定しているので、全部ビルトインみたいな感じのものは使わない。 bootstrap系のgemとsimple_formを使って、scaffoldでどこまでがんばれるかを見てみる。

対象読者

railsチュートリアル2章までを終えて、generateコマンド等が実行できる人。

バージョン

$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin14]
$ rails -v
Rails 5.0.1

サンプルなのでrubyバージョンは前後していても動くかと思います!

1. rails new〜bundle install

まずアプリケーションを作りましょう。今回は、テスト書かない、bundle installは後ほどまとめてということで-B-Tというオプションを渡します。
(詳しいオプションたちはrails new -hしていろいろ見てみましょう。)

$ rails new test_simple_form -B -T
create
create  README.md
create  Rakefile
create  config.ru
...
create  vendor/assets/stylesheets/.keep
remove  config/initializers/cors.rb
$ cd test_simple_form

次に、Bundlerで使用するGemfileをテキストエディタで編集します。以下の内容に書き換えてください。

source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end

gem 'rails', '5.0.1'
gem 'sqlite3', '1.3.13'
gem 'puma', '3.7.0'
gem 'sass-rails', '5.0.6'
gem 'uglifier', '3.0.4'
gem 'coffee-rails', '4.2.1'
gem 'jquery-rails', '4.2.2'
gem 'turbolinks', '5.0.1'
gem 'jbuilder', '2.6.1'
gem 'record_tag_helper', '1.0.0'
gem 'bootstrap-sass', '3.3.7'
gem 'simple_form', '3.4.0'

group :development, :test do
  gem 'byebug', '9.0.6', platform: :mri
end

group :development do
  gem 'bootstrap-generators', '3.3.4'
  gem 'web-console', '3.4.0'
  gem 'listen', '3.0.8'
  gem 'spring', '2.0.1'
  gem 'spring-watcher-listen', '2.0.1'
end

gemをインストールします。

$ bundle install

今回のチュートリアルではbootstrap-sasssimple_formbootstrap-generatorsの機能がみそになります。bootstrap-generatorsは今日時点でrails5対応が終わっていないようなので、generateだけやってgemfileからけしてもらってもOK

2. generate xx

各種初期化用のgenerateコマンドを実行していきます。

まずはbootstrapのスタイルシートとlayoutファイル、scaffold用のテンプレートを作成するbootstrap:installです。
途中application.html.erbを上書くかどうか聞かれますが、OKなのでYを入力してenterを押しましょう。

$ rails g bootstrap:install
...
Overwrite /path/to/app/test_simple_form/app/views/layouts/application.html.erb? (enter "h" for help) [Ynaqdh] Y
...

次にsimple_formの設定ファイルをテンプレート作るコマンドです。
今回はbootstrap用の物を作るので、--bootstrapというオプションを渡します。
途中lib/templates/erb/scaffold/_form.html.erbを上書いていいか聞かれるので、Yを押してenter。bootstrap-generatorsがつくった_form.html.erbもありますが、今回はsimple_formのものを使います。

$ rails g simple_form:install --bootstrap
...
Overwrite /path/to/app/test_simple_form/lib/templates/erb/scaffold/_form.html.erb? (enter "h" for help) [Ynaqdh] Y
...

リソース作成

scaffoldで何かリソースを作ってみましょう

$ rails g scaffold post title body:text published:boolean
$ rails db:create
$ rails db:migrate

そしてサーバを立ち上げていくつかリソースを追加してみましょう! f:id:waterlow2013:20170129234346p:plain

その他

今回のチュートリアルだけで行くと、simple_formは必要ありません。しかし、今後いろいろなformを作るにあたり、アプリケーション全体のformをsimple_formで統一しておくと色々ご利益があるということで、追加しています。

まとめ

railsで開発する場合は、最低限のものを提供するのにものすごく早いスピートで作ることが可能です。
要件が複雑な場合はそのままは使えないのですが、うまく仕事にとりいれていきたいですね。