saml2 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +6 -4
- data/exe/bulk_verify_responses +94 -0
- data/lib/saml2/assertion.rb +7 -7
- data/lib/saml2/attribute/x500.rb +31 -28
- data/lib/saml2/attribute.rb +53 -49
- data/lib/saml2/attribute_consuming_service.rb +29 -31
- data/lib/saml2/authn_request.rb +54 -47
- data/lib/saml2/authn_statement.rb +31 -20
- data/lib/saml2/base.rb +72 -63
- data/lib/saml2/bindings/http_post.rb +7 -7
- data/lib/saml2/bindings/http_redirect.rb +37 -33
- data/lib/saml2/bindings.rb +1 -1
- data/lib/saml2/conditions.rb +19 -16
- data/lib/saml2/contact.rb +19 -18
- data/lib/saml2/endpoint.rb +14 -11
- data/lib/saml2/entity.rb +27 -27
- data/lib/saml2/identity_provider.rb +13 -10
- data/lib/saml2/indexed_object.rb +15 -12
- data/lib/saml2/key.rb +43 -34
- data/lib/saml2/localized_name.rb +11 -10
- data/lib/saml2/logout_request.rb +8 -8
- data/lib/saml2/logout_response.rb +4 -4
- data/lib/saml2/message.rb +24 -20
- data/lib/saml2/name_id.rb +45 -41
- data/lib/saml2/namespaces.rb +8 -8
- data/lib/saml2/organization.rb +11 -10
- data/lib/saml2/organization_and_contacts.rb +5 -5
- data/lib/saml2/request.rb +3 -3
- data/lib/saml2/requested_authn_context.rb +4 -4
- data/lib/saml2/response.rb +45 -33
- data/lib/saml2/role.rb +11 -11
- data/lib/saml2/schemas.rb +13 -10
- data/lib/saml2/service_provider.rb +11 -12
- data/lib/saml2/signable.rb +23 -18
- data/lib/saml2/sso.rb +5 -5
- data/lib/saml2/status.rb +9 -7
- data/lib/saml2/status_response.rb +5 -5
- data/lib/saml2/subject.rb +28 -28
- data/lib/saml2/version.rb +1 -1
- data/lib/saml2.rb +7 -7
- metadata +78 -122
- data/spec/fixtures/FederationMetadata.xml +0 -670
- data/spec/fixtures/authnrequest.xml +0 -12
- data/spec/fixtures/certificate.pem +0 -24
- data/spec/fixtures/entities.xml +0 -13
- data/spec/fixtures/external-uri-reference-response.xml +0 -48
- data/spec/fixtures/identity_provider.xml +0 -46
- data/spec/fixtures/noconditions_response.xml +0 -1
- data/spec/fixtures/othercertificate.pem +0 -25
- data/spec/fixtures/privatekey.key +0 -27
- data/spec/fixtures/response_assertion_signed_reffed_from_response.xml +0 -6
- data/spec/fixtures/response_signed.xml +0 -46
- data/spec/fixtures/response_tampered_certificate.xml +0 -25
- data/spec/fixtures/response_tampered_signature.xml +0 -46
- data/spec/fixtures/response_with_attribute_signed.xml +0 -46
- data/spec/fixtures/response_with_encrypted_assertion.xml +0 -58
- data/spec/fixtures/response_with_rsa_key_value.xml +0 -1
- data/spec/fixtures/response_with_signed_assertion_and_encrypted_subject.xml +0 -116
- data/spec/fixtures/response_without_keyinfo.xml +0 -1
- data/spec/fixtures/service_provider.xml +0 -79
- data/spec/fixtures/test3-response.xml +0 -9
- data/spec/fixtures/test6-response.xml +0 -10
- data/spec/fixtures/test7-response.xml +0 -10
- data/spec/fixtures/xml_missigned_assertion.xml +0 -84
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +0 -11
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +0 -45
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +0 -44
- data/spec/fixtures/xslt-transform-response.xml +0 -57
- data/spec/lib/attribute_consuming_service_spec.rb +0 -129
- data/spec/lib/attribute_spec.rb +0 -149
- data/spec/lib/authn_request_spec.rb +0 -52
- data/spec/lib/bindings/http_redirect_spec.rb +0 -183
- data/spec/lib/conditions_spec.rb +0 -74
- data/spec/lib/entity_spec.rb +0 -58
- data/spec/lib/identity_provider_spec.rb +0 -43
- data/spec/lib/indexed_object_spec.rb +0 -71
- data/spec/lib/key_spec.rb +0 -23
- data/spec/lib/logout_request_spec.rb +0 -33
- data/spec/lib/logout_response_spec.rb +0 -33
- data/spec/lib/message_spec.rb +0 -23
- data/spec/lib/response_spec.rb +0 -293
- data/spec/lib/service_provider_spec.rb +0 -76
- data/spec/lib/signable_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6af3303cbbfd4c78c055015c30df1772bddbee535f27144696c57464398f439e
|
4
|
+
data.tar.gz: fd0fb87f88e987cf4fc081e7c4e833e12d3a7489662f314a5e5383e5f4278952
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5af19de37ce48ef31e22c18fa7eeaeb44c371d8751af579c2d2a06b6b1cd76579c64eba9b40a304a8da549937d0d7e0b9a8ef38439149be52567e0f424b85a9e
|
7
|
+
data.tar.gz: 6a224a75ff2d4f6408adf1beb3ca4fa65c15be36d65173b5882b6a0223b5b71fd3499fa4364e53f47ccba5ebd6c746fa2c2433012a0bdcd698cdb864fafd6dd8
|
data/Rakefile
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "bundler"
|
3
5
|
Bundler::GemHelper.install_tasks
|
4
6
|
|
5
|
-
require
|
7
|
+
require "rspec/core/rake_task"
|
6
8
|
RSpec::Core::RakeTask.new
|
7
9
|
|
8
|
-
task :
|
10
|
+
task default: :spec
|
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "openssl"
|
6
|
+
|
7
|
+
require "saml2"
|
8
|
+
|
9
|
+
debug = ARGV.delete("--debug")
|
10
|
+
|
11
|
+
service_provider_entity = SAML2::Entity.new
|
12
|
+
service_provider = SAML2::ServiceProvider.new
|
13
|
+
service_provider_entity.roles << service_provider
|
14
|
+
|
15
|
+
def getarg(key)
|
16
|
+
return unless (index = ARGV.index(key))
|
17
|
+
|
18
|
+
ARGV.delete_at(index)
|
19
|
+
ARGV.delete_at(index)
|
20
|
+
end
|
21
|
+
|
22
|
+
if (verification_time = getarg("--at"))
|
23
|
+
verification_time = Time.parse(verification_time)
|
24
|
+
end
|
25
|
+
verification_time ||= Time.now.utc
|
26
|
+
|
27
|
+
while (key_file = getarg("--key"))
|
28
|
+
service_provider.private_keys << OpenSSL::PKey.read(File.read(key_file))
|
29
|
+
end
|
30
|
+
|
31
|
+
while (cert_file = getarg("--certificate"))
|
32
|
+
service_provider.keys << SAML2::KeyDescriptor.new(File.read(cert_file))
|
33
|
+
end
|
34
|
+
|
35
|
+
ignored_idps = Set.new
|
36
|
+
while (idp = getarg("--ignore"))
|
37
|
+
if idp[0] == "@"
|
38
|
+
ignored_idps.merge(File.read(idp[1..]).strip.split("\n"))
|
39
|
+
else
|
40
|
+
ignored_idps << idp
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
idps = {}
|
45
|
+
if (trusted_certificates_file = getarg("--trusted-certificates"))
|
46
|
+
trusted_certificates = JSON.parse(File.read(trusted_certificates_file))
|
47
|
+
trusted_certificates.each do |(issuer, fingerprints)|
|
48
|
+
idp_entity = SAML2::Entity.new
|
49
|
+
idp_entity.entity_id = issuer
|
50
|
+
idp = SAML2::IdentityProvider.new
|
51
|
+
idp.fingerprints = fingerprints
|
52
|
+
idp_entity.roles << idp
|
53
|
+
idps[issuer] = idp_entity
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
responses = JSON.parse(File.read(ARGV.first))
|
58
|
+
index = ARGV.pop.to_i if ARGV.last.to_i.to_s == ARGV.last
|
59
|
+
|
60
|
+
bad_counts = Hash.new(0)
|
61
|
+
non_ignored_count = 0
|
62
|
+
|
63
|
+
responses = [responses[index]] if index
|
64
|
+
responses.each_with_index do |response_raw, i|
|
65
|
+
next if response_raw["SAMLResponse"].empty?
|
66
|
+
next if response_raw["error"] # we're not expected to be able to validate this
|
67
|
+
|
68
|
+
begin
|
69
|
+
puts response_raw.to_json if debug
|
70
|
+
response, _relay_state = SAML2::Bindings::HTTP_POST.decode(response_raw)
|
71
|
+
rescue => e
|
72
|
+
warn "Unable to decode '#{response_raw}' (index #{i}) due to #{e}"
|
73
|
+
next
|
74
|
+
end
|
75
|
+
|
76
|
+
next if ignored_idps.include?(response.issuer&.id)
|
77
|
+
|
78
|
+
non_ignored_count += 1
|
79
|
+
|
80
|
+
puts response.xml if debug
|
81
|
+
|
82
|
+
# TODO: ignore audience restrictions
|
83
|
+
errors = response.validate(service_provider: service_provider_entity,
|
84
|
+
identity_provider: idps[response.issuer&.id],
|
85
|
+
verification_time: verification_time)
|
86
|
+
unless errors.empty?
|
87
|
+
bad_counts[response.issuer&.id] += 1
|
88
|
+
warn "#{errors.inspect} for response #{response.id} from #{response.issuer&.id} (index #{i})"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
puts ""
|
93
|
+
puts bad_counts.sort_by(&:last).reverse.to_h.inspect
|
94
|
+
puts "#{bad_counts.values.sum}/#{non_ignored_count} failed"
|
data/lib/saml2/assertion.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "saml2/conditions"
|
4
4
|
|
5
5
|
module SAML2
|
6
6
|
class Assertion < Message
|
@@ -21,7 +21,7 @@ module SAML2
|
|
21
21
|
# @return [Subject, nil]
|
22
22
|
def subject
|
23
23
|
if xml && !instance_variable_defined?(:@subject)
|
24
|
-
@subject = Subject.from_xml(xml.at_xpath(
|
24
|
+
@subject = Subject.from_xml(xml.at_xpath("saml:Subject", Namespaces::ALL))
|
25
25
|
end
|
26
26
|
@subject
|
27
27
|
end
|
@@ -29,7 +29,7 @@ module SAML2
|
|
29
29
|
# @return [Conditions]
|
30
30
|
def conditions
|
31
31
|
if !instance_variable_defined?(:@conditions) && xml
|
32
|
-
@conditions = Conditions.from_xml(xml.at_xpath(
|
32
|
+
@conditions = Conditions.from_xml(xml.at_xpath("saml:Conditions", Namespaces::ALL))
|
33
33
|
end
|
34
34
|
@conditions
|
35
35
|
end
|
@@ -46,19 +46,19 @@ module SAML2
|
|
46
46
|
|
47
47
|
# @return [Array<AuthnStatement, AttributeStatement>]
|
48
48
|
def statements
|
49
|
-
@statements ||= load_object_array(xml,
|
49
|
+
@statements ||= load_object_array(xml, "saml:AuthnStatement|saml:AttributeStatement")
|
50
50
|
end
|
51
51
|
|
52
52
|
# (see Base#build)
|
53
53
|
def build(builder)
|
54
|
-
builder[
|
55
|
-
|
54
|
+
builder["saml"].Assertion(
|
55
|
+
"xmlns:saml" => Namespaces::SAML
|
56
56
|
) do |assertion|
|
57
57
|
super(assertion)
|
58
58
|
|
59
59
|
subject.build(assertion)
|
60
60
|
|
61
|
-
conditions
|
61
|
+
conditions&.build(assertion)
|
62
62
|
|
63
63
|
statements.each { |stmt| stmt.build(assertion) }
|
64
64
|
end
|
data/lib/saml2/attribute/x500.rb
CHANGED
@@ -3,41 +3,42 @@
|
|
3
3
|
module SAML2
|
4
4
|
class Attribute
|
5
5
|
class X500 < Attribute
|
6
|
-
GIVEN_NAME =
|
7
|
-
SN = SURNAME =
|
6
|
+
GIVEN_NAME = "urn:oid:2.5.4.42"
|
7
|
+
SN = SURNAME = "urn:oid:2.5.4.4"
|
8
8
|
# https://www.ietf.org/rfc/rfc2798.txt
|
9
9
|
module InetOrgPerson
|
10
|
-
DISPLAY_NAME =
|
11
|
-
EMPLOYEE_NUMBER =
|
12
|
-
EMPLOYEE_TYPE =
|
13
|
-
PREFERRED_LANGUAGE =
|
10
|
+
DISPLAY_NAME = "urn:oid:2.16.840.1.113730.3.1.241"
|
11
|
+
EMPLOYEE_NUMBER = "urn:oid:2.16.840.1.113730.3.1.3"
|
12
|
+
EMPLOYEE_TYPE = "urn:oid:2.16.840.1.113730.3.1.4"
|
13
|
+
PREFERRED_LANGUAGE = "urn:oid:2.16.840.1.113730.3.1.39"
|
14
14
|
end
|
15
|
+
|
15
16
|
# https://www.internet2.edu/media/medialibrary/2013/09/04/internet2-mace-dir-eduperson-201203.html
|
16
17
|
module EduPerson
|
17
|
-
AFFILIATION =
|
18
|
-
ASSURANCE =
|
19
|
-
ENTITLEMENT =
|
20
|
-
NICKNAME =
|
21
|
-
ORG_D_N =
|
22
|
-
ORG_UNIT_D_N =
|
23
|
-
PRIMARY_AFFILIATION =
|
24
|
-
PRIMARY_ORG_UNIT_D_N =
|
25
|
-
PRINCIPAL_NAME =
|
26
|
-
SCOPED_AFFILIATION =
|
27
|
-
TARGETED_I_D =
|
18
|
+
AFFILIATION = "urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
|
19
|
+
ASSURANCE = "urn:oid:1.3.6.1.4.1.5923.1.1.1.11"
|
20
|
+
ENTITLEMENT = "urn:oid:1.3.6.1.4.1.5923.1.1.1.7"
|
21
|
+
NICKNAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.2"
|
22
|
+
ORG_D_N = "urn:oid:1.3.6.1.4.1.5923.1.1.1.3"
|
23
|
+
ORG_UNIT_D_N = "urn:oid:1.3.6.1.4.1.5923.1.1.1.4"
|
24
|
+
PRIMARY_AFFILIATION = "urn:oid:1.3.6.1.4.1.5923.1.1.1.5"
|
25
|
+
PRIMARY_ORG_UNIT_D_N = "urn:oid:1.3.6.1.4.1.5923.1.1.1.8"
|
26
|
+
PRINCIPAL_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
|
27
|
+
SCOPED_AFFILIATION = "urn:oid:1.3.6.1.4.1.5923.1.1.1.9"
|
28
|
+
TARGETED_I_D = "urn:oid:1.3.6.1.4.1.5923.1.1.1.10"
|
28
29
|
end
|
29
30
|
# http://www.ietf.org/rfc/rfc4519.txt
|
30
|
-
UID = USERID =
|
31
|
+
UID = USERID = "urn:oid:0.9.2342.19200300.100.1.1"
|
31
32
|
# http://www.ietf.org/rfc/rfc4524.txt
|
32
|
-
MAIL =
|
33
|
+
MAIL = "urn:oid:0.9.2342.19200300.100.1.3"
|
33
34
|
|
34
35
|
# Returns true if the param should be an {X500} Attribute.
|
35
36
|
# @param name_or_node [String, Nokogiri::XML::Element]
|
36
37
|
def self.recognizes?(name_or_node)
|
37
38
|
if name_or_node.is_a?(Nokogiri::XML::Element)
|
38
39
|
!!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL) ||
|
39
|
-
|
40
|
-
OIDS.include?(name_or_node[
|
40
|
+
((name_or_node["NameFormat"] == NameFormats::URI || name_or_node["NameFormat"].nil?) &&
|
41
|
+
OIDS.include?(name_or_node["Name"]))
|
41
42
|
else
|
42
43
|
FRIENDLY_NAMES.include?(name_or_node) || OIDS.include?(name_or_node)
|
43
44
|
end
|
@@ -58,7 +59,8 @@ module SAML2
|
|
58
59
|
# if they pass a friendly name, infer the OID
|
59
60
|
proper_name = FRIENDLY_NAMES[name]
|
60
61
|
if proper_name
|
61
|
-
|
62
|
+
friendly_name = name
|
63
|
+
name = proper_name
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
@@ -77,25 +79,26 @@ module SAML2
|
|
77
79
|
def build(builder)
|
78
80
|
super
|
79
81
|
attr = builder.parent.last_element_child
|
80
|
-
attr.add_namespace_definition(
|
81
|
-
attr[
|
82
|
+
attr.add_namespace_definition("x500", Namespaces::X500)
|
83
|
+
attr["x500:Encoding"] = "LDAP"
|
82
84
|
end
|
83
85
|
|
84
86
|
# build hashes out of our known attribute names for quick lookup
|
85
|
-
FRIENDLY_NAMES = ([self] + constants).
|
87
|
+
FRIENDLY_NAMES = ([self] + constants).each_with_object({}) do |mod, hash|
|
86
88
|
mod = const_get(mod) unless mod.is_a?(Module)
|
87
89
|
next hash unless mod.is_a?(Module)
|
88
90
|
# Don't look in modules inherited from parent classes
|
89
|
-
next hash unless mod.name.start_with?(
|
91
|
+
next hash unless mod.name.start_with?(name)
|
92
|
+
|
90
93
|
mod.constants.each do |key|
|
91
94
|
value = mod.const_get(key)
|
92
95
|
next unless value.is_a?(String)
|
96
|
+
|
93
97
|
key = key.to_s.downcase.gsub(/_\w/) { |c| c[1].upcase }
|
94
98
|
# eduPerson prefixes all of their names
|
95
|
-
key = "eduPerson#{key.sub(/^\w
|
99
|
+
key = "eduPerson#{key.sub(/^\w/, &:upcase)}" if mod == EduPerson
|
96
100
|
hash[key] = value
|
97
101
|
end
|
98
|
-
hash
|
99
102
|
end.freeze
|
100
103
|
OIDS = FRIENDLY_NAMES.invert.freeze
|
101
104
|
private_constant :FRIENDLY_NAMES, :OIDS
|
data/lib/saml2/attribute.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "date"
|
4
4
|
|
5
|
-
require
|
5
|
+
require "active_support/core_ext/array/wrap"
|
6
6
|
|
7
|
-
require
|
8
|
-
require
|
7
|
+
require "saml2/base"
|
8
|
+
require "saml2/namespaces"
|
9
9
|
|
10
10
|
module SAML2
|
11
11
|
class Attribute < Base
|
@@ -43,13 +43,13 @@ module SAML2
|
|
43
43
|
# The XML namespace that this attribute class serializes as.
|
44
44
|
# @return ['saml']
|
45
45
|
def namespace
|
46
|
-
|
46
|
+
"saml"
|
47
47
|
end
|
48
48
|
|
49
49
|
# The XML element that this attribute class serializes as.
|
50
50
|
# @return ['Attribute']
|
51
51
|
def element
|
52
|
-
|
52
|
+
"Attribute"
|
53
53
|
end
|
54
54
|
|
55
55
|
protected
|
@@ -59,10 +59,10 @@ module SAML2
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def inherited(klass)
|
62
|
+
super
|
62
63
|
subclasses << klass
|
63
64
|
end
|
64
65
|
|
65
|
-
|
66
66
|
def class_for(name_or_node)
|
67
67
|
subclasses.find do |klass|
|
68
68
|
klass.respond_to?(:recognizes?) && klass.recognizes?(name_or_node)
|
@@ -84,18 +84,22 @@ module SAML2
|
|
84
84
|
# @param friendly_name optional [String, nil]
|
85
85
|
# @param name_format optional [String, nil]
|
86
86
|
def initialize(name = nil, value = nil, friendly_name = nil, name_format = nil)
|
87
|
-
|
87
|
+
super()
|
88
|
+
@name = name
|
89
|
+
@value = value
|
90
|
+
@friendly_name = friendly_name
|
91
|
+
@name_format = name_format
|
88
92
|
end
|
89
93
|
|
90
94
|
# (see Base#build)
|
91
95
|
def build(builder)
|
92
|
-
builder[self.class.namespace].__send__(self.class.element,
|
93
|
-
attribute.parent[
|
94
|
-
attribute.parent[
|
96
|
+
builder[self.class.namespace].__send__(self.class.element, "Name" => name) do |attribute|
|
97
|
+
attribute.parent["FriendlyName"] = friendly_name if friendly_name
|
98
|
+
attribute.parent["NameFormat"] = name_format if name_format
|
95
99
|
Array.wrap(value).each do |value|
|
96
100
|
xsi_type, val = convert_to_xsi(value)
|
97
|
-
attribute[
|
98
|
-
attribute_value.parent[
|
101
|
+
attribute["saml"].AttributeValue(val) do |attribute_value|
|
102
|
+
attribute_value.parent["xsi:type"] = xsi_type if xsi_type
|
99
103
|
end
|
100
104
|
end
|
101
105
|
end
|
@@ -104,15 +108,15 @@ module SAML2
|
|
104
108
|
# (see Base#from_xml)
|
105
109
|
def from_xml(node)
|
106
110
|
super
|
107
|
-
@name = node[
|
108
|
-
@friendly_name = node[
|
109
|
-
@name_format = node[
|
110
|
-
values = node.xpath(
|
111
|
-
convert_from_xsi(value.attribute_with_ns(
|
111
|
+
@name = node["Name"]
|
112
|
+
@friendly_name = node["FriendlyName"]
|
113
|
+
@name_format = node["NameFormat"]
|
114
|
+
values = node.xpath("saml:AttributeValue", Namespaces::ALL).map do |value|
|
115
|
+
convert_from_xsi(value.attribute_with_ns("type", Namespaces::XSI), value.content && value.content.strip)
|
112
116
|
end
|
113
117
|
@value = case values.length
|
114
|
-
when 0
|
115
|
-
when 1
|
118
|
+
when 0 then nil
|
119
|
+
when 1 then values.first
|
116
120
|
else; values
|
117
121
|
end
|
118
122
|
end
|
@@ -120,13 +124,13 @@ module SAML2
|
|
120
124
|
private
|
121
125
|
|
122
126
|
XS_TYPES = {
|
123
|
-
lookup_qname(
|
124
|
-
[[TrueClass, FalseClass], nil, ->(v) { %w
|
125
|
-
lookup_qname(
|
127
|
+
lookup_qname("xs:boolean", Namespaces::ALL) =>
|
128
|
+
[[TrueClass, FalseClass], nil, ->(v) { %w[true 1].include?(v) }],
|
129
|
+
lookup_qname("xs:string", Namespaces::ALL) =>
|
126
130
|
[String, nil, nil],
|
127
|
-
lookup_qname(
|
131
|
+
lookup_qname("xs:date", Namespaces::ALL) =>
|
128
132
|
[Date, nil, ->(v) { Date.parse(v) if v }],
|
129
|
-
lookup_qname(
|
133
|
+
lookup_qname("xs:dateTime", Namespaces::ALL) =>
|
130
134
|
[Time, ->(v) { v.iso8601 }, ->(v) { Time.parse(v) if v }]
|
131
135
|
}.freeze
|
132
136
|
|
@@ -134,11 +138,11 @@ module SAML2
|
|
134
138
|
xs_type = nil
|
135
139
|
converter = nil
|
136
140
|
XS_TYPES.each do |type, (klasses, to_xsi, _from_xsi)|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
141
|
+
next unless Array.wrap(klasses).any? { |klass| value.is_a?(klass) }
|
142
|
+
|
143
|
+
xs_type = "xs:#{type.last}"
|
144
|
+
converter = to_xsi
|
145
|
+
break
|
142
146
|
end
|
143
147
|
value = converter.call(value) if converter
|
144
148
|
[xs_type, value]
|
@@ -146,12 +150,11 @@ module SAML2
|
|
146
150
|
|
147
151
|
def convert_from_xsi(type, value)
|
148
152
|
return value unless type
|
153
|
+
|
149
154
|
qname = self.class.lookup_qname(type.value, type.namespaces)
|
150
155
|
|
151
156
|
info = XS_TYPES[qname]
|
152
|
-
|
153
|
-
value = info.last.call(value)
|
154
|
-
end
|
157
|
+
value = info.last.call(value) if info&.last
|
155
158
|
value
|
156
159
|
end
|
157
160
|
end
|
@@ -160,12 +163,13 @@ module SAML2
|
|
160
163
|
attr_reader :attributes
|
161
164
|
|
162
165
|
def initialize(attributes = [])
|
166
|
+
super()
|
163
167
|
@attributes = attributes
|
164
168
|
end
|
165
169
|
|
166
170
|
def from_xml(node)
|
167
171
|
super
|
168
|
-
@attributes = node.xpath(
|
172
|
+
@attributes = node.xpath("saml:Attribute", Namespaces::ALL).map do |attr|
|
169
173
|
Attribute.from_xml(attr)
|
170
174
|
end
|
171
175
|
end
|
@@ -190,29 +194,29 @@ module SAML2
|
|
190
194
|
|
191
195
|
prior_value = result[key]
|
192
196
|
result[key] = if prior_value
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
197
|
+
value = Array.wrap(prior_value)
|
198
|
+
# repeated key; convert to array
|
199
|
+
if attribute.value.is_a?(Array)
|
200
|
+
# both values are arrays; concatenate them
|
201
|
+
value.concat(attribute.value)
|
202
|
+
else
|
203
|
+
value << attribute.value
|
204
|
+
end
|
205
|
+
value
|
206
|
+
else
|
207
|
+
attribute.value
|
208
|
+
end
|
205
209
|
end
|
206
210
|
result
|
207
211
|
end
|
208
212
|
|
209
213
|
def build(builder)
|
210
|
-
builder[
|
211
|
-
|
214
|
+
builder["saml"].AttributeStatement("xmlns:xs" => Namespaces::XS,
|
215
|
+
"xmlns:xsi" => Namespaces::XSI) do |statement|
|
212
216
|
@attributes.each { |attr| attr.build(statement) }
|
213
217
|
end
|
214
218
|
end
|
215
219
|
end
|
216
220
|
end
|
217
221
|
|
218
|
-
require
|
222
|
+
require "saml2/attribute/x500"
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "active_support/core_ext/array/wrap"
|
4
4
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
5
|
+
require "saml2/attribute"
|
6
|
+
require "saml2/indexed_object"
|
7
|
+
require "saml2/localized_name"
|
8
|
+
require "saml2/namespaces"
|
9
9
|
|
10
10
|
module SAML2
|
11
11
|
class RequestedAttribute < Attribute
|
@@ -13,13 +13,13 @@ module SAML2
|
|
13
13
|
# The XML namespace that this attribute class serializes as.
|
14
14
|
# @return ['md']
|
15
15
|
def namespace
|
16
|
-
|
16
|
+
"md"
|
17
17
|
end
|
18
18
|
|
19
19
|
# The XML element that this attribute class serializes as.
|
20
20
|
# @return ['RequestedAttribute']
|
21
21
|
def element
|
22
|
-
|
22
|
+
"RequestedAttribute"
|
23
23
|
end
|
24
24
|
|
25
25
|
# Create a RequestAttribute object to represent an attribute.
|
@@ -51,7 +51,7 @@ module SAML2
|
|
51
51
|
# (see Base#from_xml)
|
52
52
|
def from_xml(node)
|
53
53
|
super
|
54
|
-
@is_required = node[
|
54
|
+
@is_required = node["isRequired"] && node["isRequired"] == "true"
|
55
55
|
end
|
56
56
|
|
57
57
|
# @return [true, false, nil]
|
@@ -79,9 +79,10 @@ module SAML2
|
|
79
79
|
# @param requested_attribute [RequestedAttribute]
|
80
80
|
def initialize(requested_attribute, provided_value)
|
81
81
|
super("Attribute #{requested_attribute.name} is provided value " \
|
82
|
-
|
83
|
-
|
84
|
-
@requested_attribute
|
82
|
+
"#{provided_value.inspect}, but only allows " \
|
83
|
+
"#{Array.wrap(requested_attribute.value).inspect}")
|
84
|
+
@requested_attribute = requested_attribute
|
85
|
+
@provided_value = provided_value
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
@@ -97,16 +98,16 @@ module SAML2
|
|
97
98
|
# @param requested_attributes [::Array<RequestedAttributes>]
|
98
99
|
def initialize(name = nil, requested_attributes = [])
|
99
100
|
super()
|
100
|
-
@name = LocalizedName.new(
|
101
|
-
@description = LocalizedName.new(
|
101
|
+
@name = LocalizedName.new("ServiceName", name)
|
102
|
+
@description = LocalizedName.new("ServiceDescription")
|
102
103
|
@requested_attributes = requested_attributes
|
103
104
|
end
|
104
105
|
|
105
106
|
# (see Base#from_xml)
|
106
107
|
def from_xml(node)
|
107
108
|
super
|
108
|
-
name.from_xml(node.xpath(
|
109
|
-
description.from_xml(node.xpath(
|
109
|
+
name.from_xml(node.xpath("md:ServiceName", Namespaces::ALL))
|
110
|
+
description.from_xml(node.xpath("md:ServiceDescription", Namespaces::ALL))
|
110
111
|
@requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
|
111
112
|
end
|
112
113
|
|
@@ -126,49 +127,46 @@ module SAML2
|
|
126
127
|
# If a {RequestedAttribute} is tagged as required, but it has not been
|
127
128
|
# supplied.
|
128
129
|
def create_statement(attributes)
|
129
|
-
if attributes.is_a?(Hash)
|
130
|
-
attributes = attributes.map { |k, v| Attribute.create(k, v) }
|
131
|
-
end
|
130
|
+
attributes = attributes.map { |k, v| Attribute.create(k, v) } if attributes.is_a?(Hash)
|
132
131
|
|
133
132
|
attributes_hash = {}
|
134
133
|
attributes.each do |attr|
|
135
134
|
attr.value = attr.value.call if attr.value.respond_to?(:call)
|
136
135
|
attributes_hash[[attr.name, attr.name_format]] = attr
|
137
|
-
if attr.name_format
|
138
|
-
attributes_hash[[attr.name, nil]] = attr
|
139
|
-
end
|
136
|
+
attributes_hash[[attr.name, nil]] = attr if attr.name_format
|
140
137
|
end
|
141
138
|
|
142
139
|
attributes = []
|
143
140
|
requested_attributes.each do |requested_attr|
|
144
141
|
attr = attributes_hash[[requested_attr.name, requested_attr.name_format]]
|
145
|
-
if requested_attr.name_format
|
146
|
-
attr ||= attributes_hash[[requested_attr.name, nil]]
|
147
|
-
end
|
142
|
+
attr ||= attributes_hash[[requested_attr.name, nil]] if requested_attr.name_format
|
148
143
|
if attr
|
149
144
|
if requested_attr.value &&
|
150
|
-
|
145
|
+
!Array.wrap(requested_attr.value).include?(attr.value)
|
151
146
|
raise InvalidAttributeValue.new(requested_attr, attr.value)
|
152
147
|
end
|
148
|
+
|
153
149
|
attributes << attr
|
154
150
|
elsif requested_attr.required?
|
155
151
|
# if the metadata includes only one possible value, helpfully set
|
156
152
|
# that value
|
157
|
-
|
158
|
-
|
159
|
-
requested_attr.value)
|
160
|
-
else
|
161
|
-
raise RequiredAttributeMissing.new(requested_attr)
|
153
|
+
unless requested_attr.value && !requested_attr.value.is_a?(::Array)
|
154
|
+
raise RequiredAttributeMissing, requested_attr
|
162
155
|
end
|
156
|
+
|
157
|
+
attributes << Attribute.create(requested_attr.name,
|
158
|
+
requested_attr.value)
|
159
|
+
|
163
160
|
end
|
164
161
|
end
|
165
162
|
return nil if attributes.empty?
|
163
|
+
|
166
164
|
AttributeStatement.new(attributes)
|
167
165
|
end
|
168
166
|
|
169
167
|
# (see Base#build)
|
170
168
|
def build(builder)
|
171
|
-
builder[
|
169
|
+
builder["md"].AttributeConsumingService do |attribute_consuming_service|
|
172
170
|
name.build(attribute_consuming_service)
|
173
171
|
description.build(attribute_consuming_service)
|
174
172
|
requested_attributes.each do |requested_attribute|
|