main
  1# frozen_string_literal: true
  2
  3module Saml
  4  module Kit
  5    # This class is a base class for SAML documents.
  6    class Document
  7      include Validatable
  8      include Buildable
  9      include Translatable
 10      include Trustable
 11      include XmlParseable
 12      include XsdValidatable
 13
 14      attr_accessor :registry
 15      attr_reader :name
 16      validates_presence_of :content
 17      validates_presence_of :id
 18      validate :must_match_xsd
 19      validate :must_be_expected_type
 20      validate :must_be_valid_version
 21
 22      def initialize(xml, name:, configuration: Saml::Kit.configuration)
 23        @configuration = configuration
 24        @registry = configuration.registry
 25        @content = xml
 26        @name = name
 27      end
 28
 29      # Returns the ID for the SAML document.
 30      def id
 31        at_xpath('./*/@ID').try(:value)
 32      end
 33
 34      # Returns the Issuer for the SAML document.
 35      def issuer
 36        at_xpath('./*/saml:Issuer').try(:text)
 37      end
 38
 39      # Returns the Version of the SAML document.
 40      def version
 41        at_xpath('./*/@Version').try(:value)
 42      end
 43
 44      # Returns the Destination of the SAML document.
 45      def destination
 46        at_xpath('./*/@Destination').try(:value)
 47      end
 48
 49      # Returns the Destination of the SAML document.
 50      def issue_instant
 51        Time.parse(at_xpath('./*/@IssueInstant').try(:value))
 52      end
 53
 54      class << self
 55        CONSTRUCTORS = {
 56          'Assertion' => -> { Saml::Kit::Assertion },
 57          'AuthnRequest' => -> { Saml::Kit::AuthenticationRequest },
 58          'LogoutRequest' => -> { Saml::Kit::LogoutRequest },
 59          'LogoutResponse' => -> { Saml::Kit::LogoutResponse },
 60          'Response' => -> { Saml::Kit::Response },
 61        }.freeze
 62        XPATH = [
 63          '/saml:Assertion',
 64          '/samlp:AuthnRequest',
 65          '/samlp:LogoutRequest',
 66          '/samlp:LogoutResponse',
 67          '/samlp:Response',
 68        ].join('|')
 69
 70        # Returns the raw xml as a Saml::Kit SAML document.
 71        #
 72        # @param xml [String] the raw xml string.
 73        # @param configuration [Saml::Kit::Configuration] configuration to use
 74        # for unpacking the document.
 75        def to_saml_document(xml, configuration: Saml::Kit.configuration)
 76          namespaces = {
 77            saml: Namespaces::ASSERTION,
 78            samlp: Namespaces::PROTOCOL,
 79          }
 80          element = Nokogiri::XML(xml).at_xpath(XPATH, namespaces)
 81          constructor = CONSTRUCTORS[element.name].try(:call) || InvalidDocument
 82          constructor.new(xml, configuration: configuration)
 83        rescue StandardError => error
 84          Saml::Kit.logger.error(error)
 85          InvalidDocument.new(xml, configuration: configuration)
 86        end
 87
 88        # @!visibility private
 89        def builder_class # :nodoc:
 90          {
 91            Assertion.to_s => Saml::Kit::Builders::Assertion,
 92            AuthenticationRequest.to_s => Saml::Kit::Builders::AuthenticationRequest,
 93            LogoutRequest.to_s => Saml::Kit::Builders::LogoutRequest,
 94            LogoutResponse.to_s => Saml::Kit::Builders::LogoutResponse,
 95            Response.to_s => Saml::Kit::Builders::Response,
 96          }[name] || (raise ArgumentError, "Unknown SAML Document #{name}")
 97        end
 98      end
 99
100      private
101
102      attr_reader :content, :configuration
103
104      def must_match_xsd
105        matches_xsd?(PROTOCOL_XSD)
106      end
107
108      def must_be_expected_type
109        errors.add(:base, error_message(:invalid)) unless expected_type?
110      end
111
112      def expected_type?
113        at_xpath("./samlp:#{name}").present?
114      end
115
116      def must_be_valid_version
117        return unless expected_type?
118        return if version == '2.0'
119
120        errors.add(:version, error_message(:invalid_version))
121      end
122    end
123  end
124end