devise_saml_authenticatableのSamlAuthenticatable::SamlResponseのソースコードを追ってみる
こんにちは!kossyです!
今回は、前回のブログで追いきれなかった、SamlAuthenticatable::SamlResponseのソースコードを追ってみようと思います。
前回のブログ
kossy-web-engineer.hatenablog.com
環境
Ruby 2.6.8
Rails 6.0.4
MacOS BigSur
devise_saml_authenticatable 1.7.0
呼び出し元のおさらい
呼び出し元の処理はこちらでした。
def authenticate_with_saml(saml_response, relay_state) key = Devise.saml_default_user_key decorated_response = ::SamlAuthenticatable::SamlResponse.new( saml_response, attribute_map(saml_response), ) if Devise.saml_use_subject auth_value = saml_response.name_id else auth_value = decorated_response.attribute_value_by_resource_key(key) end auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key) resource = Devise.saml_resource_locator.call(self, decorated_response, auth_value) raise "Only one validator configuration can be used at a time" if Devise.saml_resource_validator && Devise.saml_resource_validator_hook if Devise.saml_resource_validator || Devise.saml_resource_validator_hook valid = if Devise.saml_resource_validator then Devise.saml_resource_validator.new.validate(resource, saml_response) else Devise.saml_resource_validator_hook.call(resource, decorated_response, auth_value) end if !valid logger.info("User(#{auth_value}) did not pass custom validation.") return nil end end if resource.nil? if Devise.saml_create_user logger.info("Creating user(#{auth_value}).") resource = new else logger.info("User(#{auth_value}) not found. Not configured to create the user.") return nil end end if Devise.saml_update_user || (resource.new_record? && Devise.saml_create_user) Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value) end resource end
authenticate_with_samlメソッドの中で、SamlAuthenticatable::SamlResponse.newが呼び出されていましたね。
SamlAuthenticatable::SamlResponseクラス
改行を含めても16行の小さいクラスでした。
require 'devise_saml_authenticatable/saml_mapped_attributes' module SamlAuthenticatable class SamlResponse attr_reader :raw_response, :attributes def initialize(saml_response, attribute_map) @attributes = ::SamlAuthenticatable::SamlMappedAttributes.new(saml_response.attributes, attribute_map) @raw_response = saml_response end def attribute_value_by_resource_key(key) attributes.value_by_resource_key(key) end end end
initializeメソッドの中で SamlAuthenticatable::SamlMappedAttributesというクラスがnewされていますので、処理を見てみます。
と思ったんですが、authenticate_with_samlメソッドの中でattribute_mapというメソッドが呼ばれてますね、、、こちらから見てみましょうか。
attribute_map
定義元はSamlAuthenticatableモジュールの84行目です。
def attribute_map(saml_response = nil) attribute_map_resolver.new(saml_response).attribute_map end def attribute_map_resolver if Devise.saml_attribute_map_resolver.respond_to?(:new) Devise.saml_attribute_map_resolver else Devise.saml_attribute_map_resolver.constantize end end
Devise.saml_attribute_map_resolverに対して、newが定義されていれば Devise.saml_attribute_map_resolver.newを呼び出し、
newが定義されていなければ Devise.saml_attribute_map_resolverをcontantizeメソッドで文字列をクラス化した後にnewを呼び出していました。
module DeviseSamlAuthenticatable class DefaultAttributeMapResolver def initialize(saml_response) @saml_response = saml_response end def attribute_map return {} unless File.exist?(attribute_map_path) attribute_map = YAML.load(File.read(attribute_map_path)) if attribute_map.key?(Rails.env) attribute_map[Rails.env] else attribute_map end end private attr_reader :saml_response def attribute_map_path Rails.root.join("config", "attribute-map.yml") end end end
initializeメソッドでは引数で渡されたsaml_responseをインスタンス変数に代入しているだけですね。
attribute_mapメソッドは、configディレクトリ直下にattribute_map.ymlファイルが存在していなければ {} を返却し、
存在していれば、YAML.loadで読み出して、attribute_mal.ymlにproduction: のように環境毎に定義が分かれていれば、その環境の値を読み出し、
定義が分かれていなければ全てのattribute_map.ymlの値を返しています。この実装方法、他にも応用が効きそうな気がしています。
attribute_map.ymlファイルの中身はこんな感じになるかと思います。
"urn:mace:dir:attribute-def:email": "email"
SAMLの属性とdeviseの属性をmappingする用途で使っています。
この辺りはコンソールで試してみましょうか。
# Instead of storing the attribute_map in attribute-map.yml, store it in the database, or set it programatically # attribute_mapをattribute-map.ymlに保存する代わりに、データベースに保存するか、プログラムで設定します # 私の環境ではfalseが返りました。 $ Devise.saml_attribute_map_resolver.respond_to?(:new) => false # newは DeviseSamlAuthenticatable::DefaultAttributeMapResolver に対して呼び出される $ Devise.saml_attribute_map_resolver.constantize => DeviseSamlAuthenticatable::DefaultAttributeMapResolver # 私の環境ではattribute-map.ymlを定義しているので返り値が存在する $ attribute_map_path => #<Pathname:/app/config/attribute-map.yml> $ File.exist?(attribute_map_path) => true $ attribute_map => {"urn:mace:dir:attribute-def:email"=>"email"}
attribute_mapメソッドの動きが理解できたところで、 SamlAuthenticatable::SamlResponseクラスに戻ります。
SamlAuthenticatable::SamlMappedAttributesクラス
deviseとsaml_responseのattrのmappingを担当するクラスですね。
module SamlAuthenticatable class SamlMappedAttributes def initialize(attributes, attribute_map) @attributes = attributes @attribute_map = attribute_map end def saml_attribute_keys @attribute_map.keys end def resource_keys @attribute_map.values end def value_by_resource_key(key) str_key = String(key) # Find all of the SAML attributes that map to the resource key attribute_map_for_key = @attribute_map.select { |_, resource_key| String(resource_key) == str_key } saml_value = nil # Find the first non-nil value attribute_map_for_key.each_key do |saml_key| saml_value = value_by_saml_attribute_key(saml_key) break unless saml_value.nil? end saml_value end def value_by_saml_attribute_key(key) @attributes[String(key)] end end end
initialize時に引数で渡ってきたSamlResponseの属性を基に、attribute_mapの値と比較しつつ、合致した値を返却するような処理が含まれていました。
こちらもコンソールで試してみましょう。
$ decorated_response.attribute_value_by_resource_key(key) => nil # stepでメソッド内部に入り込んでなぜnilが返るのか検証してみました 16: def value_by_resource_key(key) => 17: str_key = String(key) 18: 19: # Find all of the SAML attributes that map to the resource key 20: attribute_map_for_key = @attribute_map.select { |_, resource_key| String(resource_key) == str_key } 21: 22: saml_value = nil 23: 24: # Find the first non-nil value 25: attribute_map_for_key.each_key do |saml_key| 26: saml_value = value_by_saml_attribute_key(saml_key) 27: 28: break unless saml_value.nil? 29: end 30: 31: saml_value 32: end $ key => :email $ str_key = String(key) => "email" $ attribute_map_for_key = @attribute_map.select { |_, resource_key| String(resource_key) == str_key } => {"urn:mace:dir:attribute-def:email"=>"email"} 34: def value_by_saml_attribute_key(key) => 35: @attributes[String(key)] 36: end # @attributesがそもそも{}だったからでした $ @attributes[String(key)] => nil
概ね挙動の理解ができました。