maestrano-ruby-test 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +45 -0
  5. data/LICENSE +21 -0
  6. data/README.md +794 -0
  7. data/Rakefile +40 -0
  8. data/bin/maestrano-console +9 -0
  9. data/lib/maestrano.rb +271 -0
  10. data/lib/maestrano/account/bill.rb +14 -0
  11. data/lib/maestrano/account/recurring_bill.rb +14 -0
  12. data/lib/maestrano/api/error/authentication_error.rb +8 -0
  13. data/lib/maestrano/api/error/base_error.rb +24 -0
  14. data/lib/maestrano/api/error/connection_error.rb +8 -0
  15. data/lib/maestrano/api/error/invalid_request_error.rb +14 -0
  16. data/lib/maestrano/api/list_object.rb +37 -0
  17. data/lib/maestrano/api/object.rb +187 -0
  18. data/lib/maestrano/api/operation/base.rb +215 -0
  19. data/lib/maestrano/api/operation/create.rb +18 -0
  20. data/lib/maestrano/api/operation/delete.rb +13 -0
  21. data/lib/maestrano/api/operation/list.rb +18 -0
  22. data/lib/maestrano/api/operation/update.rb +59 -0
  23. data/lib/maestrano/api/resource.rb +47 -0
  24. data/lib/maestrano/api/util.rb +122 -0
  25. data/lib/maestrano/open_struct.rb +11 -0
  26. data/lib/maestrano/saml/attribute_value.rb +15 -0
  27. data/lib/maestrano/saml/metadata.rb +64 -0
  28. data/lib/maestrano/saml/request.rb +93 -0
  29. data/lib/maestrano/saml/response.rb +201 -0
  30. data/lib/maestrano/saml/schemas/saml20assertion_schema.xsd +283 -0
  31. data/lib/maestrano/saml/schemas/saml20protocol_schema.xsd +302 -0
  32. data/lib/maestrano/saml/schemas/xenc_schema.xsd +146 -0
  33. data/lib/maestrano/saml/schemas/xmldsig_schema.xsd +318 -0
  34. data/lib/maestrano/saml/settings.rb +37 -0
  35. data/lib/maestrano/saml/validation_error.rb +7 -0
  36. data/lib/maestrano/sso.rb +86 -0
  37. data/lib/maestrano/sso/base_group.rb +31 -0
  38. data/lib/maestrano/sso/base_membership.rb +25 -0
  39. data/lib/maestrano/sso/base_user.rb +75 -0
  40. data/lib/maestrano/sso/group.rb +24 -0
  41. data/lib/maestrano/sso/session.rb +107 -0
  42. data/lib/maestrano/sso/user.rb +34 -0
  43. data/lib/maestrano/version.rb +3 -0
  44. data/lib/maestrano/xml_security/signed_document.rb +170 -0
  45. data/maestrano.gemspec +32 -0
  46. data/maestrano.png +0 -0
  47. data/test/helpers/api_helpers.rb +115 -0
  48. data/test/helpers/saml_helpers.rb +62 -0
  49. data/test/maestrano/account/bill_test.rb +48 -0
  50. data/test/maestrano/account/recurring_bill_test.rb +49 -0
  51. data/test/maestrano/api/list_object_test.rb +20 -0
  52. data/test/maestrano/api/object_test.rb +28 -0
  53. data/test/maestrano/api/resource_test.rb +343 -0
  54. data/test/maestrano/api/util_test.rb +31 -0
  55. data/test/maestrano/maestrano_test.rb +260 -0
  56. data/test/maestrano/open_struct_test.rb +10 -0
  57. data/test/maestrano/saml/request_test.rb +168 -0
  58. data/test/maestrano/saml/response_test.rb +290 -0
  59. data/test/maestrano/saml/settings_test.rb +51 -0
  60. data/test/maestrano/sso/base_group_test.rb +54 -0
  61. data/test/maestrano/sso/base_membership_test.rb +45 -0
  62. data/test/maestrano/sso/base_user_test.rb +114 -0
  63. data/test/maestrano/sso/group_test.rb +47 -0
  64. data/test/maestrano/sso/session_test.rb +161 -0
  65. data/test/maestrano/sso/user_test.rb +65 -0
  66. data/test/maestrano/sso_test.rb +105 -0
  67. data/test/maestrano/xml_security/signed_document.rb +163 -0
  68. data/test/support/saml/certificates/certificate1 +12 -0
  69. data/test/support/saml/certificates/r1_certificate2_base64 +1 -0
  70. data/test/support/saml/responses/adfs_response_sha1.xml +46 -0
  71. data/test/support/saml/responses/adfs_response_sha256.xml +46 -0
  72. data/test/support/saml/responses/adfs_response_sha384.xml +46 -0
  73. data/test/support/saml/responses/adfs_response_sha512.xml +46 -0
  74. data/test/support/saml/responses/no_signature_ns.xml +48 -0
  75. data/test/support/saml/responses/open_saml_response.xml +56 -0
  76. data/test/support/saml/responses/r1_response6.xml.base64 +1 -0
  77. data/test/support/saml/responses/response1.xml.base64 +1 -0
  78. data/test/support/saml/responses/response2.xml.base64 +79 -0
  79. data/test/support/saml/responses/response3.xml.base64 +66 -0
  80. data/test/support/saml/responses/response4.xml.base64 +93 -0
  81. data/test/support/saml/responses/response5.xml.base64 +102 -0
  82. data/test/support/saml/responses/response_with_ampersands.xml +139 -0
  83. data/test/support/saml/responses/response_with_ampersands.xml.base64 +93 -0
  84. data/test/support/saml/responses/response_with_multiple_attribute_values.xml +57 -0
  85. data/test/support/saml/responses/simple_saml_php.xml +71 -0
  86. data/test/support/saml/responses/starfield_response.xml.base64 +1 -0
  87. data/test/support/saml/responses/wrapped_response_2.xml.base64 +150 -0
  88. data/test/test_helper.rb +47 -0
  89. metadata +315 -0
@@ -0,0 +1,59 @@
1
+ module Maestrano
2
+ module API
3
+ module Operation
4
+ module Update
5
+ def save(opts={})
6
+ values = serialize_params(self).merge(opts)
7
+
8
+ if @values[:metadata]
9
+ values[:metadata] = serialize_metadata
10
+ end
11
+
12
+ if values.length > 0
13
+ values.delete(:id)
14
+
15
+ response, api_token = Maestrano::API::Operation::Base.request(:put, url, @api_token, values)
16
+ refresh_from(response, api_token)
17
+ end
18
+ self
19
+ end
20
+
21
+ def serialize_metadata
22
+ if @unsaved_values.include?(:metadata)
23
+ # the metadata object has been reassigned
24
+ # i.e. as object.metadata = {key => val}
25
+ metadata_update = @values[:metadata] # new hash
26
+ new_keys = metadata_update.keys.map(&:to_sym)
27
+ # remove keys at the server, but not known locally
28
+ keys_to_unset = @previous_metadata.keys - new_keys
29
+ keys_to_unset.each {|key| metadata_update[key] = ''}
30
+
31
+ metadata_update
32
+ else
33
+ # metadata is a Maestrano::API::Object, and can be serialized normally
34
+ serialize_params(@values[:metadata])
35
+ end
36
+ end
37
+
38
+ def serialize_params(obj)
39
+ case obj
40
+ when nil
41
+ ''
42
+ when Maestrano::API::Object
43
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
44
+ obj_values = obj.instance_variable_get(:@values)
45
+ update_hash = {}
46
+
47
+ unsaved_keys.each do |k|
48
+ update_hash[k] = serialize_params(obj_values[k])
49
+ end
50
+
51
+ update_hash
52
+ else
53
+ obj
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ module Maestrano
2
+ module API
3
+ class Resource < Maestrano::API::Object
4
+ def self.class_name
5
+ self.name.split('::').reject { |w| w.to_s == "Maestrano" }
6
+ end
7
+
8
+ def self.url
9
+ if self == Maestrano::API::Resource
10
+ raise NotImplementedError.new('Maestrano::API::Resource is an abstract class. You should perform actions on its subclasses (Bill, Customer, etc.)')
11
+ end
12
+ if class_name.is_a?(Array)
13
+ class_name.map { |w| CGI.escape(self.underscore(w)) }.join("/") + 's'
14
+ else
15
+ "#{CGI.escape(self.underscore(class_name))}s"
16
+ end
17
+ end
18
+
19
+ def url
20
+ unless id = self['id']
21
+ raise Maestrano::API::Error::InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
22
+ end
23
+ "#{self.class.url}/#{CGI.escape(id)}"
24
+ end
25
+
26
+ def refresh
27
+ response, api_token = Maestrano::API::Operation::Base.request(:get, url, @api_token, @retrieve_options)
28
+ refresh_from(response, api_token)
29
+ self
30
+ end
31
+
32
+ def self.retrieve(id, api_token=nil)
33
+ instance = self.new(id, api_token)
34
+ instance.refresh
35
+ instance
36
+ end
37
+
38
+ def self.underscore(string_val)
39
+ string_val.gsub(/::/, '/').
40
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
41
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
42
+ tr("-", "_").
43
+ downcase
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,122 @@
1
+ module Maestrano
2
+ module API
3
+ module Util
4
+ def self.objects_to_ids(h)
5
+ case h
6
+ when Maestrano::API::Resource
7
+ h.id
8
+ when Hash
9
+ res = {}
10
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
11
+ res
12
+ when Array
13
+ h.map { |v| objects_to_ids(v) }
14
+ when Time
15
+ h.iso8601
16
+ else
17
+ h
18
+ end
19
+ end
20
+
21
+ def self.object_classes
22
+ @object_classes ||= {
23
+ 'account_bill' => Maestrano::Account::Bill,
24
+ 'account_recurring_bill' => Maestrano::Account::RecurringBill,
25
+ 'internal_list_object' => Maestrano::API::ListObject
26
+ }
27
+ end
28
+
29
+ def self.convert_to_maestrano_object(resp, api_token)
30
+ case resp
31
+ when Array
32
+ if resp.empty? || !resp.first[:object]
33
+ resp
34
+ else
35
+ list = convert_to_maestrano_object({
36
+ object: 'internal_list_object',
37
+ data:[],
38
+ url: convert_to_maestrano_object(resp.first, api_token).class.url
39
+ },api_token)
40
+
41
+ resp.each do |i|
42
+ list.data.push(convert_to_maestrano_object(i, api_token))
43
+ end
44
+ list
45
+ end
46
+ when Hash
47
+ # Try converting to a known object class. If none available, fall back to generic Maestrano::API::Object
48
+ object_classes.fetch(resp[:object], Maestrano::API::Object).construct_from(resp, api_token)
49
+ else
50
+ # Automatically parse iso8601 dates
51
+ if resp =~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
52
+ Time.iso8601(resp).utc
53
+ else
54
+ resp
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.file_readable(file)
60
+ # This is nominally equivalent to File.readable?, but that can
61
+ # report incorrect results on some more oddball filesystems
62
+ # (such as AFS)
63
+ begin
64
+ File.open(file) { |f| }
65
+ rescue
66
+ false
67
+ else
68
+ true
69
+ end
70
+ end
71
+
72
+ def self.symbolize_names(object)
73
+ case object
74
+ when Hash
75
+ new = {}
76
+ object.each do |key, value|
77
+ key = (key.to_sym rescue key) || key
78
+ new[key] = symbolize_names(value)
79
+ end
80
+ new
81
+ when Array
82
+ object.map { |value| symbolize_names(value) }
83
+ else
84
+ object
85
+ end
86
+ end
87
+
88
+ def self.url_encode(key)
89
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
90
+ end
91
+
92
+ def self.flatten_params(params, parent_key=nil)
93
+ result = []
94
+ params.each do |key, value|
95
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
96
+ if value.is_a?(Hash)
97
+ result += flatten_params(value, calculated_key)
98
+ elsif value.is_a?(Array)
99
+ result += flatten_params_array(value, calculated_key)
100
+ else
101
+ result << [calculated_key, value]
102
+ end
103
+ end
104
+ result
105
+ end
106
+
107
+ def self.flatten_params_array(value, calculated_key)
108
+ result = []
109
+ value.each do |elem|
110
+ if elem.is_a?(Hash)
111
+ result += flatten_params(elem, calculated_key)
112
+ elsif elem.is_a?(Array)
113
+ result += flatten_params_array(elem, calculated_key)
114
+ else
115
+ result << ["#{calculated_key}[]", elem]
116
+ end
117
+ end
118
+ result
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,11 @@
1
+ module Maestrano
2
+
3
+ # Extebd OpenStruct to include a 'attributes'
4
+ # method
5
+ class OpenStruct < ::OpenStruct
6
+ # Return all object defined attributes
7
+ def attributes
8
+ (self.methods - self.class.new.methods).reject {|method| method =~ /=$/ }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Maestrano
2
+ module Saml
3
+
4
+ # Wrapper for AttributeValue with multiple values
5
+ # It is subclass of String to be backwards compatible
6
+ # Use AttributeValue#values to get all values as an array
7
+ class AttributeValue < String
8
+ attr_accessor :values
9
+ def initialize(str="", values=[])
10
+ @values = values
11
+ super(str.to_s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,64 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+ require "uri"
4
+
5
+ # Class to return SP metadata based on the settings requested.
6
+ # Return this XML in a controller, then give that URL to the the
7
+ # IdP administrator. The IdP will poll the URL and your settings
8
+ # will be updated automatically
9
+ module Maestrano
10
+ module Saml
11
+ include REXML
12
+ class Metadata
13
+ def generate(settings)
14
+ meta_doc = REXML::Document.new
15
+ root = meta_doc.add_element "md:EntityDescriptor", {
16
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
17
+ }
18
+ sp_sso = root.add_element "md:SPSSODescriptor", {
19
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
20
+ # Metadata request need not be signed (as we don't publish our cert)
21
+ "AuthnRequestsSigned" => false,
22
+ # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
23
+ "WantAssertionsSigned" => (!settings.idp_cert_fingerprint.nil? || !settings.idp_cert.nil?)
24
+ }
25
+ if settings.issuer != nil
26
+ root.attributes["entityID"] = settings.issuer
27
+ end
28
+ if settings.assertion_consumer_logout_service_url != nil
29
+ sp_sso.add_element "md:SingleLogoutService", {
30
+ # Add this as a setting to create different bindings?
31
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
32
+ "Location" => settings.assertion_consumer_logout_service_url,
33
+ "ResponseLocation" => settings.assertion_consumer_logout_service_url,
34
+ "isDefault" => true,
35
+ "index" => 0
36
+ }
37
+ end
38
+ if settings.name_identifier_format != nil
39
+ name_id = sp_sso.add_element "md:NameIDFormat"
40
+ name_id.text = settings.name_identifier_format
41
+ end
42
+ if settings.assertion_consumer_service_url != nil
43
+ sp_sso.add_element "md:AssertionConsumerService", {
44
+ # Add this as a setting to create different bindings?
45
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
46
+ "Location" => settings.assertion_consumer_service_url,
47
+ "isDefault" => true,
48
+ "index" => 0
49
+ }
50
+ end
51
+ # With OpenSSO, it might be required to also include
52
+ # <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
53
+ # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
54
+
55
+ meta_doc << REXML::XMLDecl.new
56
+ ret = ""
57
+ # pretty print the XML so IdP administrators can easily see what the SP supports
58
+ meta_doc.write(ret, 1)
59
+
60
+ ret
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,93 @@
1
+ require "base64"
2
+ require "uuid"
3
+ require "zlib"
4
+ require "cgi"
5
+ require "rexml/document"
6
+ require "rexml/xpath"
7
+
8
+ module Maestrano
9
+ module Saml
10
+ include REXML
11
+ class Request
12
+ attr_accessor :settings, :params, :session
13
+
14
+ def initialize(params = {}, session = {})
15
+ self.settings = Maestrano::SSO.saml_settings
16
+ self.params = params
17
+ self.session = session
18
+ end
19
+
20
+ def redirect_url
21
+ request_doc = create_authentication_xml_doc(settings)
22
+ request_doc.context[:attribute_quote] = :quote if self.settings.double_quote_xml_attribute_values
23
+
24
+ request = ""
25
+ request_doc.write(request)
26
+
27
+ request = Zlib::Deflate.deflate(request, 9)[2..-5] if self.settings.compress_request
28
+ base64_request = Base64.encode64(request)
29
+ encoded_request = CGI.escape(base64_request)
30
+ params_prefix = (self.settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
31
+ request_params = "#{params_prefix}SAMLRequest=#{encoded_request}"
32
+
33
+ self.params.each_pair do |key, value|
34
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
35
+ end
36
+
37
+ if (request_params !~ /group_id=/) && (group_id = (self.session[:mno_group_uid] || self.session['mno_group_uid']))
38
+ request_params << "&group_id=#{CGI.escape(group_id.to_s)}"
39
+ end
40
+
41
+ self.settings.idp_sso_target_url + request_params
42
+ end
43
+
44
+ def create_authentication_xml_doc(settings)
45
+ uuid = "_" + UUID.new.generate
46
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
47
+ # Create AuthnRequest root element using REXML
48
+ request_doc = REXML::Document.new
49
+
50
+ root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
51
+ root.attributes['ID'] = uuid
52
+ root.attributes['IssueInstant'] = time
53
+ root.attributes['Version'] = "2.0"
54
+ root.attributes['Destination'] = self.settings.idp_sso_target_url unless self.settings.idp_sso_target_url.nil?
55
+ root.attributes['IsPassive'] = self.settings.passive unless self.settings.passive.nil?
56
+ root.attributes['ProtocolBinding'] = self.settings.protocol_binding unless self.settings.protocol_binding.nil?
57
+
58
+ # Conditionally defined elements based on settings
59
+ if self.settings.assertion_consumer_service_url != nil
60
+ root.attributes["AssertionConsumerServiceURL"] = self.settings.assertion_consumer_service_url
61
+ end
62
+ if self.settings.issuer != nil
63
+ issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
64
+ issuer.text = self.settings.issuer
65
+ end
66
+ if self.settings.name_identifier_format != nil
67
+ root.add_element "samlp:NameIDPolicy", {
68
+ "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
69
+ # Might want to make AllowCreate a setting?
70
+ "AllowCreate" => "true",
71
+ "Format" => self.settings.name_identifier_format
72
+ }
73
+ end
74
+
75
+ # BUG fix here -- if an authn_context is defined, add the tags with an "exact"
76
+ # match required for authentication to succeed. If this is not defined,
77
+ # the IdP will choose default rules for authentication. (Shibboleth IdP)
78
+ if self.settings.authn_context != nil
79
+ requested_context = root.add_element "samlp:RequestedAuthnContext", {
80
+ "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
81
+ "Comparison" => "exact",
82
+ }
83
+ class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
84
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
85
+ }
86
+ class_ref.text = self.settings.authn_context
87
+ end
88
+ request_doc
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,201 @@
1
+ require "time"
2
+ require "nokogiri"
3
+
4
+ # Only supports SAML 2.0
5
+ module Maestrano
6
+ module Saml
7
+
8
+ class Response
9
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
10
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
11
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
12
+
13
+ # TODO: This should probably be ctor initialized too... WDYT?
14
+ attr_accessor :settings
15
+
16
+ attr_reader :options
17
+ attr_reader :response
18
+ attr_reader :document
19
+
20
+ def initialize(response, options = {})
21
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
22
+ @options = options
23
+ @response = (response =~ /^</) ? response : Base64.decode64(response)
24
+ @document = Maestrano::XMLSecurity::SignedDocument.new(@response)
25
+ @settings = Maestrano::SSO.saml_settings
26
+ end
27
+
28
+ def is_valid?
29
+ validate
30
+ end
31
+
32
+ def validate!
33
+ validate(false)
34
+ end
35
+
36
+ # The value of the user identifier as designated by the initialization request response
37
+ def name_id
38
+ @name_id ||= begin
39
+ node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
40
+ node.nil? ? nil : node.text
41
+ end
42
+ end
43
+
44
+ def sessionindex
45
+ @sessionindex ||= begin
46
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
47
+ node.nil? ? nil : node.attributes['SessionIndex']
48
+ end
49
+ end
50
+
51
+ # A hash of all the attributes with the response.
52
+ # Multiple values will be returned in the AttributeValue#values array
53
+ # in reverse order, when compared to XML
54
+ def attributes
55
+ @attr_statements ||= begin
56
+ result = {}
57
+
58
+ stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
59
+ return {} if stmt_element.nil?
60
+
61
+ stmt_element.elements.each do |attr_element|
62
+ name = attr_element.attributes["Name"]
63
+ values = attr_element.elements.collect(&:text)
64
+
65
+ # Set up a string-like wrapper for the values array
66
+ attr_value = AttributeValue.new(values.first, values.reverse)
67
+ # Merge values if the Attribute has already been seen
68
+ if result[name]
69
+ attr_value.values += result[name].values
70
+ end
71
+
72
+ result[name] = attr_value
73
+ end
74
+
75
+ result.keys.each do |key|
76
+ result[key.intern] = result[key]
77
+ end
78
+
79
+ result
80
+ end
81
+ end
82
+
83
+ # When this user session should expire at latest
84
+ def session_expires_at
85
+ @expires_at ||= begin
86
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
87
+ parse_time(node, "SessionNotOnOrAfter")
88
+ end
89
+ end
90
+
91
+ # Checks the status of the response for a "Success" code
92
+ def success?
93
+ @status_code ||= begin
94
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
95
+ node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
96
+ end
97
+ end
98
+
99
+ # Conditions (if any) for the assertion to run
100
+ def conditions
101
+ @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
102
+ end
103
+
104
+ def not_before
105
+ @not_before ||= parse_time(conditions, "NotBefore")
106
+ end
107
+
108
+ def not_on_or_after
109
+ @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
110
+ end
111
+
112
+ def issuer
113
+ @issuer ||= begin
114
+ node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
115
+ node ||= xpath_first_from_signed_assertion('/a:Issuer')
116
+ node.nil? ? nil : node.text
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def validation_error(message)
123
+ raise ValidationError.new(message)
124
+ end
125
+
126
+ def validate(soft = true)
127
+ validate_structure(soft) &&
128
+ validate_response_state(soft) &&
129
+ validate_conditions(soft) &&
130
+ document.validate_document(get_fingerprint, soft) &&
131
+ success?
132
+ end
133
+
134
+ def validate_structure(soft = true)
135
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), 'schemas'))) do
136
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
137
+ @xml = Nokogiri::XML(self.document.to_s)
138
+ end
139
+ if soft
140
+ @schema.validate(@xml).map{ return false }
141
+ else
142
+ @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
143
+ end
144
+ end
145
+
146
+ def validate_response_state(soft = true)
147
+ if response.empty?
148
+ return soft ? false : validation_error("Blank response")
149
+ end
150
+
151
+ if settings.nil?
152
+ return soft ? false : validation_error("No settings on response")
153
+ end
154
+
155
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
156
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
157
+ end
158
+
159
+ true
160
+ end
161
+
162
+ def xpath_first_from_signed_assertion(subelt=nil)
163
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
164
+ node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id}']/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
165
+ node
166
+ end
167
+
168
+ def get_fingerprint
169
+ if settings.idp_cert
170
+ cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
171
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
172
+ else
173
+ settings.idp_cert_fingerprint
174
+ end
175
+ end
176
+
177
+ def validate_conditions(soft = true)
178
+ return true if conditions.nil?
179
+ return true if options[:skip_conditions]
180
+
181
+ now = Time.now.utc
182
+
183
+ if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
184
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
185
+ end
186
+
187
+ if not_on_or_after && now >= not_on_or_after
188
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
189
+ end
190
+
191
+ true
192
+ end
193
+
194
+ def parse_time(node, attribute)
195
+ if node && node.attributes[attribute]
196
+ Time.parse(node.attributes[attribute])
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end