Rack::CorsでCORSの設定

SinatraやRailsでCORSの設定をする時に、よくRack::Corsを利用します。
その時の設定と注意点を。

SinatraでRack::Corsを使う

SinatraでRack::Corsを使う時は、READMEに書かれている通りです。

www.khasegawa.netからのリクエストを許可する

例として、このサイト(www.khasegawa.net)からのリクエストを許可するようにしてみます。
originsにwww.khasegawa.netと単一で指定するだけですね。

use Rack::Cors do
  allow do
    origins 'www.khasegawa.net'
    resource '*', methods: %i[get]
  end
end

これでリクエストが通るようになりました。
この時のAccess-Control-Allow–Originは、https://www.khasegawa.netになり、期待する値になっていることも確認出来ますね。

他のドメインからも許可する

他のドメイン(example.com)からもリクエストを許可します。
originsメソッドの引数は、可変長引数となっているため、example.comを追加します。

use Rack::Cors do
  allow do
    origins 'www.khasegawa.net', 'example.com'
    resource '*', methods: %i[get]
  end
end

これでリクエストが通るようになりました。
簡単ですね。

この時のAccess-Control-Allow–Originは、「www.khasegawa.net」からリクエストした場合、https://www.khasegawa.netになり、「example.com」からのリクエストの場合は、https://example.comという期待する値になっていることも確認出来ます。

複数指定した場合は、マッチしたホストが設定されています。(正確にはschemeやポートを含めてものだが)

www.khasegawa.netとsub.khasegawa.netを許可する

Rack::Corsでは、引数に正規表現を指定することも出来るので、それを利用します。

use Rack::Cors do
  allow do
    origins /\Ahttps:\/\/(www|sub)\.khasegawa\.net\z/
    resource '*', methods: %i[get]
  end
end

正規表現で指定する場合の注意点としては、セキュリティを考慮し、schemeを含めた完全一致にすることをオススメします。

この時のAccess-Control-Allow–Originは、「www.khasegawa.net」からリクエストした場合、https://www.khasegawa.netになり、「sub.khasegawa.com」からのリクエストの場合は、https://sub.khasegawa.comという期待する値になっていることも確認出来ますね。

正規表現を指定した場合は、マッチしたホストを返却するようになっています。

挙動から分かる注意点

これらから分かることはなんでしょうか。
どれだけ指定しても、Access-Control-Allow-Originに設定される値は、1つなのです。(許可されていない場合は何も設定されない)
ということは、どこかでレスポンスをキャッシュすると不都合が出そうです。

これは、READMEのCommon Gotchasに書いてあることからも分かります。
Common GotchasのCachingでは、Rack::Cacheの話しか出ていませんが、要はキャッシュさせないでっていうことですね。

こういうのは、見落としがちだったりするので要注意ですね。

Ansibleでgem installする時の注意

Ansibleでgem installしても、参照出来なくて困った時の話。

Playbook

とりあえず、Playbookを貼ります。。

---
- hosts: all
  become: false
  tasks:
    - name: Download rbenv
      git:
        repo: https://github.com/rbenv/rbenv.git
        dest: /home/vagrant/.rbenv
        update: no
    - name: Download rbenv-build
      git:
        repo: https://github.com/sstephenson/ruby-build.git
        dest: /home/vagrant/.rbenv/plugins/ruby-build
        update: no
    - name: Set rbenv environment
      register: result
      changed_when: '"Set rbenv environment" in result.stdout'
      shell: |
        if [ "$(grep rbenv ${HOME}/.bashrc)" == "" ]; then
          echo 'Set rbenv environment'
          echo 'export PATH="${HOME}/.rbenv/shims:${HOME}/.rbenv/bin:${PATH}"' >> ${HOME}/.bashrc
        fi
    - name: Install ruby
      register: ruby_installer_result
      changed_when: '"Installing ruby" in ruby_installer_result.stdout'
      shell: |
        if [ "$(rbenv versions 2>&1 | grep 2.4.1)" == "" ]; then
          echo 'Installing ruby 2.4.1'
          ${HOME}/.rbenv/bin/rbenv install 2.4.1
          ${HOME}/.rbenv/bin/rbenv global 2.4.1
          ${HOME}/.rbenv/bin/rbenv rehash
        fi
    - name: Install bundler
      gem:
        name: bundler
        state: present
        executable: ${HOME}/.rbenv/shims/gem
        version: 1.15.3
    - name: Install rails
      gem:
        name: rails
        state: present
        executable: ${HOME}/.rbenv/shims/gem
        version: 5.1.3

余りキレイな書き方ではないのですが、

  1. ホームディレクトリ配下にrbenvをインストール
  2. ruby-buildを追加
  3. rbenv周りの環境変数を.bashrcに追記
  4. Rubyのインストール
  5. bundlerのインストール
  6. railsのインストール

をしているだけです。
このPlaybookを実行しても、bundleやrailsが見つかりません。
が、gem listにはあったのです。

まさか…

–user-install

gem listにあるということは、どこかにインストールはされているわけです。
今の設定で、参照出来なくてインストールされているとなると、可能性が高いのは、–user-installオプションをつけてインストールしているときです。

AnsibleのgemモジュールのOptionsを見てみます。

–user-installに該当するオプションがありますね。
user_installです。
どうやら、デフォルト値がtrueになっているようです。

ということは、これをfalseで指定してあげればいいですね。

---
- hosts: all
  become: false
  tasks:
    - name: Download rbenv
      git:
        repo: https://github.com/rbenv/rbenv.git
        dest: /home/vagrant/.rbenv
        update: no
    - name: Download rbenv-build
      git:
        repo: https://github.com/sstephenson/ruby-build.git
        dest: /home/vagrant/.rbenv/plugins/ruby-build
        update: no
    - name: Set rbenv environment
      register: result
      changed_when: '"Set rbenv environment" in result.stdout'
      shell: |
        if [ "$(grep rbenv ${HOME}/.bashrc)" == "" ]; then
          echo 'Set rbenv environment'
          echo 'export PATH="${HOME}/.rbenv/shims:${HOME}/.rbenv/bin:${PATH}"' >> ${HOME}/.bashrc
        fi
    - name: Install ruby
      register: ruby_installer_result
      changed_when: '"Installing ruby" in ruby_installer_result.stdout'
      shell: |
        if [ "$(rbenv versions 2>&1 | grep 2.4.1)" == "" ]; then
          echo 'Installing ruby 2.4.1'
          ${HOME}/.rbenv/bin/rbenv install 2.4.1
          ${HOME}/.rbenv/bin/rbenv global 2.4.1
          ${HOME}/.rbenv/bin/rbenv rehash
        fi
    - name: Install bundler
      gem:
        name: bundler
        state: present
        executable: ${HOME}/.rbenv/shims/gem
        user_install: false # 追記
        version: 1.15.3
    - name: Install rails
      gem:
        name: rails
        state: present
        executable: ${HOME}/.rbenv/shims/gem
        user_install: false # 追記
        version: 5.1.3

これで期待するところにインストールされるようになりました。
一安心一安心

Devise関連のControllerの親クラス

BigQueryを使う仕事が、ちょっと暇になってしまったので、またRails周りの記事を少し。

Devise関連のControllerの親クラス

Ruby on Railsを使っている人達では有名なDeviseですが、その中で使われているControllerが何を継承しているのか追ってみます。(2017年7月19日 v4.3.0時点)

Devise::SessionsController

まずは、Deviseを使う時に必ず見かけると言っても過言ではないDevise::SessionControllerを見ましょうかか。
このクラスの定義はapp/controllers/devise/sessions_controller.rbになります。

先頭の数行を抜粋してみてみると、

class Devise::SessionsController < DeviseController
  prepend_before_action :require_no_authentication, only: [:new, :create]
  prepend_before_action :allow_params_authentication!, only: :create
  prepend_before_action :verify_signed_out_user, only: :destroy
  prepend_before_action only: [:create, :destroy] { request.env["devise.skip_timeout"] = true }

DeviseControllerを継承していますね。
それでは、次にDeviseControlelrの定義を見ましょうか。

DeviseController

このクラスの定義はapp/controllers/devise_controller.rbです。

こちらも抜粋。

# All Devise controllers are inherited from here.
class DeviseController < Devise.parent_controller.constantize
  include Devise::Controllers::ScopedViews

どうやら、DeviseControllerの親クラスは、Devise.parent_controller.constantizeで評価時に動的に決まっているようです。
それじゃ、このparent_controllerの中身は一体何なのでしょうか?

Devise

Deviseは、lib/devise.rbで定義されています。
こちらで、parent_controllerを探してみましょう。

では、抜粋!

  # The parent controller all Devise controllers inherits from.
  # Defaults to ApplicationController. This should be set early
  # in the initialization process and should be set to a string.
  mattr_accessor :parent_controller
  @@parent_controller = "ApplicationController"

はい、定義が見つかりましたね。
parent_controllerの中身は、ApplicationControllerという文字列のようです。
つまり、ApplicationControllerがDeviseControllerの親クラスでした。

そのため、ApplicationControllerで定義しているメソッドなどがDevise::SessionsControllerなどでも利用可能であり、コールバックを定義しているなら実行されてしまうわけです。

DeviseControllerの親クラスを変更する

こちらは、難しいこともないです。
抜粋したコード内のコメントを見てもらうと分かるのですが、initializerで変更することが可能です。

変える時は、以下をinitializerに追記してください。

config.parent_controller = 'OtherController'

簡単ですよね。

最後に

業務のRailsアプリでは、1つのアプリ内に管理画面とフロント画面のCMSを構築することも多いです。
となると、それぞれのApplicationControllerが定義されたりすることがあります。
そういう時にDevise関連のControllerの親クラスを意識せずに使用して、意図しない動作になりうることもあります。
今回は、その一例です。
Gemを利用する場合は、しっかり使う範囲だけで良いので、挙動を把握しておくといいかもしれませんね。

管理画面とフロント側を凄く分けたい…