saml2 3.1.2 → 3.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 -137
- data/schemas/MetadataExchange.xsd +0 -112
- data/schemas/metadata_combined.xsd +0 -13
- data/schemas/oasis-200401-wss-wssecurity-secext-1.0.xsd +0 -195
- data/schemas/oasis-200401-wss-wssecurity-utility-1.0.xsd +0 -108
- data/schemas/saml-schema-assertion-2.0.xsd +0 -283
- data/schemas/saml-schema-metadata-2.0.xsd +0 -339
- data/schemas/saml-schema-protocol-2.0.xsd +0 -302
- data/schemas/sstc-saml-metadata-ext-query.xsd +0 -66
- data/schemas/ws-addr.xsd +0 -137
- data/schemas/ws-authorization.xsd +0 -145
- data/schemas/ws-federation.xsd +0 -471
- data/schemas/ws-securitypolicy-1.2.xsd +0 -1205
- data/schemas/xenc-schema.xsd +0 -136
- data/schemas/xml.xsd +0 -287
- data/schemas/xmldsig-core-schema.xsd +0 -309
- 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: 8f10ad7e5f4379ecbb273d27aed0db3b58ac1db6115b54f2199662169b0ee808
|
4
|
+
data.tar.gz: 7842786be374bb809438567a58abcda27c88b5953512787b1d7a9338cf8e9901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ad080a07e9083b9c4734319e4344e81e0866c8d73c08da9d55312866607f6820141c161875905726f5bc8aeaa36ca50f79fb4c8bcf334c03f71b498a692d68b
|
7
|
+
data.tar.gz: a8905fd54b6137a4474857155e40288798d8fdeb033282dbfbf37bbcc0740d1c12080efc5e4a0f83bdc9a91ace3da2f1b71f9290d7019c386179e82fb3f2acb4
|
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|
|