RubyのOpenStrictクラスの使い方をGemのコードで理解を試みる
こんにちは!kossyです!
今回は、RubyのOpenStrictクラスの使い方を saml-idp という Gem のコード内での使い方を見て、理解を深めてみたいと思います。
ドキュメントを見る
公式のOpenStrictクラスのドキュメントを見てみます。
要素を動的に追加・削除できる手軽な構造体を提供するクラスです。
OpenStruct のインスタンスに対して未定義なメソッド x= を呼ぶと、 OpenStruct クラスの BasicObject#method_missing で捕捉され、そのインスタンスにインスタンスメソッド x, x= が定義されます。
この挙動によって要素を動的に変更できる構造体として働きます。出典: https://docs.ruby-lang.org/ja/latest/class/OpenStruct.html
「動的にメソッドを生やしたり削除したりできる」ところを抑えればよさそうですね。
かなり実装の自由度が高いように感じますが、saml-idp Gem 内でどのように使われているかを見てみます。
SamlIdp::Configurator
# encoding: utf-8 require 'ostruct' require 'securerandom' module SamlIdp class Configurator attr_accessor :x509_certificate attr_accessor :secret_key attr_accessor :password attr_accessor :algorithm attr_accessor :organization_name attr_accessor :organization_url attr_accessor :base_saml_location attr_accessor :entity_id attr_accessor :reference_id_generator attr_accessor :attribute_service_location attr_accessor :single_service_post_location attr_accessor :single_service_redirect_location attr_accessor :single_logout_service_post_location attr_accessor :single_logout_service_redirect_location attr_accessor :attributes attr_accessor :service_provider attr_accessor :assertion_consumer_service_hosts attr_accessor :session_expiry def initialize self.x509_certificate = Default::X509_CERTIFICATE self.secret_key = Default::SECRET_KEY self.algorithm = :sha1 self.reference_id_generator = ->() { SecureRandom.uuid } self.service_provider = OpenStruct.new self.service_provider.finder = ->(_) { Default::SERVICE_PROVIDER } self.service_provider.metadata_persister = ->(id, settings) { } self.service_provider.persisted_metadata_getter = ->(id, service_provider) { } self.session_expiry = 0 self.attributes = {} end # formats # getter def name_id @name_id ||= OpenStruct.new end def technical_contact @technical_contact ||= TechnicalContact.new end class TechnicalContact < OpenStruct def mail_to_string "mailto:#{email_address}" if email_address.to_s.length > 0 end end end end
initializeメソッド内で service_provider 属性に対して、OpenStrictクラスのインスタンスが代入されています。
service_providerに対して未定義のメソッドを呼び出す箇所は、ReadMeのConfigration Sectionに記載されていました。
# `identifier` is the entity_id or issuer of the Service Provider, # settings is an IncomingMetadata object which has a to_h method that needs to be persisted config.service_provider.metadata_persister = ->(identifier, settings) { fname = identifier.to_s.gsub(/\/|:/,"_") FileUtils.mkdir_p(Rails.root.join('cache', 'saml', 'metadata').to_s) File.open Rails.root.join("cache/saml/metadata/#{fname}"), "r+b" do |f| Marshal.dump settings.to_h, f end } # `identifier` is the entity_id or issuer of the Service Provider, # `service_provider` is a ServiceProvider object. Based on the `identifier` or the # `service_provider` you should return the settings.to_h from above config.service_provider.persisted_metadata_getter = ->(identifier, service_provider){ fname = identifier.to_s.gsub(/\/|:/,"_") FileUtils.mkdir_p(Rails.root.join('cache', 'saml', 'metadata').to_s) full_filename = Rails.root.join("cache/saml/metadata/#{fname}") if File.file?(full_filename) File.open full_filename, "rb" do |f| Marshal.load f end end } # Find ServiceProvider metadata_url and fingerprint based on our settings config.service_provider.finder = ->(issuer_or_entity_id) do service_providers[issuer_or_entity_id] end
metadata_persister と persisted_metadata_getter と finder メソッドが動的に定義されていました。
name_idメソッドも実体はOpenStrictインスタンスなので、使用例を見てみましょう。
# Principal (e.g. User) is passed in when you `encode_response` # # config.name_id.formats = # { # All 2.0 # email_address: -> (principal) { principal.email_address }, # transient: -> (principal) { principal.id }, # persistent: -> (p) { p.id }, # } # OR # # { # "1.1" => { # email_address: -> (principal) { principal.email_address }, # }, # "2.0" => { # transient: -> (principal) { principal.email_address }, # persistent: -> (p) { p.id }, # }, # }
TechnicalContactクラスもOpenStrictクラスを継承したクラスとして定義されていて、こちらも動的に定義している箇所がありました。
## EXAMPLE ## # config.attributes = { # GivenName: { # getter: :first_name, # }, # SurName: { # getter: :last_name, # }, # } ## EXAMPLE ## # config.technical_contact.company = "Example" # config.technical_contact.given_name = "Jonny" # config.technical_contact.sur_name = "Support" # config.technical_contact.telephone = "55555555555" # config.technical_contact.email_address = "example@example.com"
こちらはユーザーが自由に定義できるユーザー情報を入れることを期待しているようですね。
service_provider属性にOpenStrictクラスを使う理由
これは個人的な推測ですが、 SAML認証のServiceProviderとして設定できる値は十数以上あり、
「その全てをServiceProviderクラスとして定義してしまうよりは、ユーザー側に必要な値だけ定義させれば良い」
という考えでservice_provider属性にOpenStrictクラスを使ったのかと思われます。
リンク先はruby-saml Gem で service_providerに設定できる値です。
まとめ
OpenStrictクラスを使ったコードを読んでみましたが、
「クラスとしてガチガチに定義するほどでもない時」
「使う側にメソッドを動的に定義させたい時」
「具体的にクラスに持たせたい属性が決まってない時」
などで使えるのではないかと思いました。