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