saml2 1.0.0
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 +7 -0
- data/Rakefile +13 -0
- data/app/views/saml2/http_post.html.erb +14 -0
- data/lib/saml2.rb +9 -0
- data/lib/saml2/assertion.rb +37 -0
- data/lib/saml2/attribute.rb +127 -0
- data/lib/saml2/attribute/x500.rb +79 -0
- data/lib/saml2/attribute_consuming_service.rb +76 -0
- data/lib/saml2/authn_request.rb +116 -0
- data/lib/saml2/authn_statement.rb +26 -0
- data/lib/saml2/base.rb +53 -0
- data/lib/saml2/contact.rb +50 -0
- data/lib/saml2/endpoint.rb +46 -0
- data/lib/saml2/engine.rb +4 -0
- data/lib/saml2/entity.rb +84 -0
- data/lib/saml2/identity_provider.rb +57 -0
- data/lib/saml2/indexed_object.rb +59 -0
- data/lib/saml2/key.rb +46 -0
- data/lib/saml2/name_id.rb +60 -0
- data/lib/saml2/namespaces.rb +21 -0
- data/lib/saml2/organization.rb +74 -0
- data/lib/saml2/organization_and_contacts.rb +35 -0
- data/lib/saml2/profiles.rb +7 -0
- data/lib/saml2/response.rb +92 -0
- data/lib/saml2/role.rb +53 -0
- data/lib/saml2/schemas.rb +18 -0
- data/lib/saml2/service_provider.rb +30 -0
- data/lib/saml2/sso.rb +36 -0
- data/lib/saml2/subject.rb +49 -0
- data/lib/saml2/version.rb +3 -0
- data/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/schemas/saml-schema-metadata-2.0.xsd +339 -0
- data/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/schemas/xenc-schema.xsd +136 -0
- data/schemas/xml.xsd +287 -0
- data/schemas/xmldsig-core-schema.xsd +309 -0
- data/spec/fixtures/authnrequest.xml +12 -0
- data/spec/fixtures/calculated.txt +1 -0
- data/spec/fixtures/certificate.pem +25 -0
- data/spec/fixtures/entities.xml +13 -0
- data/spec/fixtures/privatekey.key +27 -0
- data/spec/fixtures/response_signed.xml +47 -0
- data/spec/fixtures/response_with_attribute_signed.xml +47 -0
- data/spec/fixtures/service_provider.xml +79 -0
- data/spec/fixtures/xmlsec.txt +1 -0
- data/spec/lib/attribute_consuming_service_spec.rb +74 -0
- data/spec/lib/attribute_spec.rb +39 -0
- data/spec/lib/authn_request_spec.rb +52 -0
- data/spec/lib/entity_spec.rb +45 -0
- data/spec/lib/identity_provider_spec.rb +23 -0
- data/spec/lib/indexed_object_spec.rb +38 -0
- data/spec/lib/response_spec.rb +60 -0
- data/spec/lib/service_provider_spec.rb +30 -0
- data/spec/spec_helper.rb +6 -0
- metadata +191 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module SAML2
|
2
|
+
class AuthnStatement
|
3
|
+
module Classes
|
4
|
+
INTERNET_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol".freeze # IP address
|
5
|
+
INTERNET_PROTOCOL_PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword".freeze # IP address, as well as username/password
|
6
|
+
KERBEROS = "urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos".freeze
|
7
|
+
PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password".freeze # username/password, NOT over SSL
|
8
|
+
PASSWORD_PROTECTED_TRANSPORT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport".freeze # username/password over SSL
|
9
|
+
PREVIOUS_SESSION = "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession".freeze # remember me
|
10
|
+
SMARTCARD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard".freeze
|
11
|
+
SMARTCARD_PKI = "urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI".freeze # smartcard with a private key on it
|
12
|
+
TLS_CLIENT = "urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient".freeze # SSL client certificate
|
13
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified".freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :authn_instant, :authn_context_class_ref
|
17
|
+
|
18
|
+
def build(builder)
|
19
|
+
builder['saml'].AuthnStatement('AuthnInstant' => authn_instant.iso8601) do |builder|
|
20
|
+
builder['saml'].AuthnContext do |builder|
|
21
|
+
builder['saml'].AuthnContextClassRef(authn_context_class_ref) if authn_context_class_ref
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/saml2/base.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Base
|
5
|
+
def self.from_xml(node)
|
6
|
+
return nil unless node
|
7
|
+
new.from_xml(node)
|
8
|
+
end
|
9
|
+
|
10
|
+
def from_xml(node)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
# make sure to not FORMAT it - it breaks signatures!
|
16
|
+
to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_xml
|
20
|
+
unless @document
|
21
|
+
builder = Nokogiri::XML::Builder.new
|
22
|
+
build(builder)
|
23
|
+
@document = builder.doc
|
24
|
+
end
|
25
|
+
@document
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load_string_array(node, element)
|
29
|
+
node.xpath(element, Namespaces::ALL).map do |node|
|
30
|
+
node.content && node.content.strip
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.load_object_array(node, element, klass)
|
35
|
+
node.xpath(element, Namespaces::ALL).map do |node|
|
36
|
+
if klass.is_a?(Hash)
|
37
|
+
klass[node.name].from_xml(node)
|
38
|
+
else
|
39
|
+
klass.from_xml(node)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def load_string_array(node, element)
|
46
|
+
self.class.load_string_array(node, element)
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_object_array(node, element, klass)
|
50
|
+
self.class.load_object_array(node, element, klass)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'saml2/base'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Contact
|
5
|
+
module Type
|
6
|
+
ADMINISTRATIVE = 'administrative'.freeze
|
7
|
+
BILLING = 'billing'.freeze
|
8
|
+
OTHER = 'other'.freeze
|
9
|
+
SUPPORT = 'support'.freeze
|
10
|
+
TECHNICAL = 'technical'.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :type, :company, :given_name, :surname, :email_addresses, :telephone_numbers
|
14
|
+
|
15
|
+
def self.from_xml(node)
|
16
|
+
return nil unless node
|
17
|
+
|
18
|
+
result = new(node['contactType'])
|
19
|
+
company = node.at_xpath('md:Company', Namespaces::ALL)
|
20
|
+
result.company = company && company.content && company.content.strip
|
21
|
+
given_name = node.at_xpath('md:GivenName', Namespaces::ALL)
|
22
|
+
result.given_name = given_name && given_name.content && given_name.content.strip
|
23
|
+
surname = node.at_xpath('md:SurName', Namespaces::ALL)
|
24
|
+
result.surname = surname && surname.content && surname.content.strip
|
25
|
+
result.email_addresses = Base.load_string_array(node, 'md:EmailAddress')
|
26
|
+
result.telephone_numbers = Base.load_string_array(node, 'md:TelephoneNumber')
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(type = Type::OTHER)
|
31
|
+
@type = type
|
32
|
+
@email_addresses = []
|
33
|
+
@telephone_numbers = []
|
34
|
+
end
|
35
|
+
|
36
|
+
def build(builder)
|
37
|
+
builder['md'].ContactPerson('contactType' => type) do |builder|
|
38
|
+
builder['md'].Company(company) if company
|
39
|
+
builder['md'].GivenName(given_name) if given_name
|
40
|
+
builder['md'].SurName(surname) if surname
|
41
|
+
email_addresses.each do |email|
|
42
|
+
builder['md'].EmailAddress(email)
|
43
|
+
end
|
44
|
+
telephone_numbers.each do |tel|
|
45
|
+
builder['md'].TelephoneNumber(tel)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module SAML2
|
2
|
+
class Endpoint < Base
|
3
|
+
module Bindings
|
4
|
+
HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze
|
5
|
+
HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
module Encodings
|
9
|
+
DEFLATE= "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE".freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :location, :binding
|
13
|
+
|
14
|
+
def initialize(location, binding = Bindings::HTTP_POST)
|
15
|
+
@location, @binding = location, binding
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(rhs)
|
19
|
+
location == rhs.location && binding == rhs.binding
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_xml(node)
|
23
|
+
@location = node['Location']
|
24
|
+
@binding = node['Binding']
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def build(builder, element)
|
29
|
+
builder['md'].__send__(element, 'Location' => location, 'Binding' => binding)
|
30
|
+
end
|
31
|
+
|
32
|
+
class Indexed < Endpoint
|
33
|
+
include IndexedObject
|
34
|
+
|
35
|
+
def initialize(location = nil, index = nil, is_default = false, binding = Bindings::HTTP_POST)
|
36
|
+
super(location, binding)
|
37
|
+
@index, @is_default = index, is_default
|
38
|
+
end
|
39
|
+
|
40
|
+
def eql?(rhs)
|
41
|
+
location == rhs.location &&
|
42
|
+
binding == rhs.binding && super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/saml2/engine.rb
ADDED
data/lib/saml2/entity.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
require 'saml2/base'
|
4
|
+
require 'saml2/identity_provider'
|
5
|
+
require 'saml2/organization_and_contacts'
|
6
|
+
require 'saml2/service_provider'
|
7
|
+
|
8
|
+
module SAML2
|
9
|
+
class Entity < Base
|
10
|
+
include OrganizationAndContacts
|
11
|
+
|
12
|
+
attr_writer :entity_id
|
13
|
+
|
14
|
+
def self.parse(xml)
|
15
|
+
document = Nokogiri::XML(xml)
|
16
|
+
|
17
|
+
# Root can be an array (EntitiesDescriptor), or a single Entity (EntityDescriptor)
|
18
|
+
entities = document.at_xpath("/md:EntitiesDescriptor", Namespaces::ALL)
|
19
|
+
entity = document.at_xpath("/md:EntityDescriptor", Namespaces::ALL)
|
20
|
+
if entities
|
21
|
+
Group.from_xml(entities)
|
22
|
+
elsif entity
|
23
|
+
from_xml(entity)
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Group < Array
|
30
|
+
def self.from_xml(node)
|
31
|
+
node && new(node)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(root)
|
35
|
+
@root = root
|
36
|
+
replace(Base.load_object_array(@root, "md:EntityDescriptor|md:EntitiesDescriptor",
|
37
|
+
'EntityDescriptor' => Entity,
|
38
|
+
'EntitiesDescriptor' => Group))
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_schema?
|
42
|
+
Schemas.metadata.valid?(@root.document)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.from_xml(node)
|
47
|
+
node && new(node)
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(root = nil)
|
51
|
+
super
|
52
|
+
@root = root
|
53
|
+
unless @root
|
54
|
+
@roles = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_schema?
|
59
|
+
Schemas.metadata.valid?(@root.document)
|
60
|
+
end
|
61
|
+
|
62
|
+
def entity_id
|
63
|
+
@entity_id || @root && @root['entityID']
|
64
|
+
end
|
65
|
+
|
66
|
+
def roles
|
67
|
+
# TODO: load IdPSSODescriptors as well
|
68
|
+
@roles ||= load_object_array(@root, 'md:SPSSODescriptor', ServiceProvider)
|
69
|
+
end
|
70
|
+
|
71
|
+
def build(builder)
|
72
|
+
builder['md'].EntityDescriptor('entityID' => entity_id,
|
73
|
+
'xmlns:md' => Namespaces::METADATA,
|
74
|
+
'xmlns:dsig' => Namespaces::DSIG,
|
75
|
+
'xmlns:xenc' => Namespaces::XENC) do |builder|
|
76
|
+
roles.each do |role|
|
77
|
+
role.build(builder)
|
78
|
+
end
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'saml2/attribute'
|
2
|
+
require 'saml2/sso'
|
3
|
+
|
4
|
+
module SAML2
|
5
|
+
class IdentityProvider < SSO
|
6
|
+
attr_writer :want_authn_requests_signed, :single_sign_on_services, :attribute_profiles, :attributes
|
7
|
+
|
8
|
+
def initialize(node = nil)
|
9
|
+
super(node)
|
10
|
+
unless node
|
11
|
+
@want_authn_requests_signed = nil
|
12
|
+
@single_sign_on_services = []
|
13
|
+
@attribute_profiles = []
|
14
|
+
@attributes = []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def want_authn_requests_signed?
|
19
|
+
unless instance_variable_defined?(:@want_authn_requests_signed)
|
20
|
+
@want_authn_requests_signed = @root['WantAuthnRequestsSigned'] && @root['WantAuthnRequestsSigned'] == 'true'
|
21
|
+
end
|
22
|
+
@want_authn_requests_signed
|
23
|
+
end
|
24
|
+
|
25
|
+
def single_sign_on_services
|
26
|
+
@single_sign_on_services ||= load_object_array(@root, 'md:SingleSignOnService', Endpoint)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attribute_profiles
|
30
|
+
@attribute_profiles ||= load_string_array(@root, 'md:AttributeProfile')
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes
|
34
|
+
@attributes ||= load_object_array(@root, 'saml:Attribute', Attribute)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build(builder)
|
38
|
+
builder['md'].IDPSSODescriptor do |builder|
|
39
|
+
super(builder)
|
40
|
+
|
41
|
+
builder['WantAuthnRequestsSigned'] = want_authn_requests_signed? unless want_authn_requests_signed?.nil?
|
42
|
+
|
43
|
+
single_sign_on_services.each do |sso|
|
44
|
+
sso.build(builder, 'SingleSignOnService')
|
45
|
+
end
|
46
|
+
|
47
|
+
attribute_profiles.each do |ap|
|
48
|
+
builder['md'].AttributeProfile(ap)
|
49
|
+
end
|
50
|
+
|
51
|
+
attributes.each do |attr|
|
52
|
+
attr.build(builder)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'saml2/base'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
module IndexedObject
|
5
|
+
attr_reader :index
|
6
|
+
|
7
|
+
def eql?(rhs)
|
8
|
+
index == rhs.index &&
|
9
|
+
default? == rhs.default? &&
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def default?
|
14
|
+
@is_default
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_xml(node)
|
18
|
+
@index = node['index'] && node['index'].to_i
|
19
|
+
@is_default = node['isDefault'] && node['isDefault'] == 'true'
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
class Array < ::Array
|
24
|
+
attr_reader :default
|
25
|
+
|
26
|
+
def self.from_xml(nodes)
|
27
|
+
new(nodes.map { |node| name.split('::')[1..-2].inject(SAML2) { |mod, klass| mod.const_get(klass) }.from_xml(node) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(objects)
|
31
|
+
replace(objects.sort_by { |object| object.index || 0 })
|
32
|
+
@index = {}
|
33
|
+
each { |object| @index[object.index] = object }
|
34
|
+
@default = find { |object| object.default? } || first
|
35
|
+
|
36
|
+
freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](index)
|
40
|
+
@index[index]
|
41
|
+
end
|
42
|
+
|
43
|
+
def resolve(index)
|
44
|
+
index ? self[index] : default
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build(builder)
|
49
|
+
super
|
50
|
+
builder.parent.last['index'] = index
|
51
|
+
builder.parent.last['isDefault'] = default? unless default?.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def self.included(klass)
|
56
|
+
klass.const_set(:Array, Array.dup)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/saml2/key.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class Key
|
5
|
+
module Type
|
6
|
+
ENCRYPTION = 'encryption'.freeze
|
7
|
+
SIGNING = 'signing'.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :use, :x509, :encryption_methods
|
11
|
+
|
12
|
+
def self.from_xml(node)
|
13
|
+
return nil unless node
|
14
|
+
|
15
|
+
x509 = node.at_xpath('dsig:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)
|
16
|
+
methods = node.xpath('xenc:EncryptionMethod', Namespaces::ALL)
|
17
|
+
new(x509 && x509.content.strip, node['use'], methods.map { |m| m['Algorithm'] })
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(x509, use = nil, encryption_methods = [])
|
21
|
+
@use, @x509, @encryption_methods = use, x509, encryption_methods
|
22
|
+
end
|
23
|
+
|
24
|
+
def encryption?
|
25
|
+
use.nil? || use == Type::ENCRYPTION
|
26
|
+
end
|
27
|
+
|
28
|
+
def signing?
|
29
|
+
use.nil? || use == Type::SIGNING
|
30
|
+
end
|
31
|
+
|
32
|
+
def build(builder)
|
33
|
+
builder['md'].KeyDescriptor do |builder|
|
34
|
+
builder.parent['use'] = use if use
|
35
|
+
builder['dsig'].KeyInfo do |builder|
|
36
|
+
builder['dsig'].X509Data do |builder|
|
37
|
+
builder['dsig'].X509Certificate(x509)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
encryption_methods.each do |method|
|
41
|
+
builder['xenc'].EncryptionMethod('Algorithm' => method)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'saml2/namespaces'
|
2
|
+
|
3
|
+
module SAML2
|
4
|
+
class NameID
|
5
|
+
module Format
|
6
|
+
EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".freeze
|
7
|
+
ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity".freeze
|
8
|
+
KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos".freeze # name[/instance]@REALM
|
9
|
+
PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent".freeze # opaque, pseudo-random, unique per SP-IdP pair
|
10
|
+
TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient".freeze # opaque, will likely change
|
11
|
+
UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".freeze
|
12
|
+
WINDOWS_DOMAIN_QUALIFIED_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName".freeze # [DomainName\]UserName
|
13
|
+
X509_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName".freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
class Policy
|
17
|
+
attr_reader :format
|
18
|
+
|
19
|
+
def self.from_xml(node)
|
20
|
+
if node
|
21
|
+
allow_create = node['AllowCreate'].nil? ? nil : node['AllowCreate'] == 'true'
|
22
|
+
NameID::Policy.new(allow_create, node['Format'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(allow_create, format)
|
27
|
+
@allow_create, @format = allow_create, format
|
28
|
+
end
|
29
|
+
|
30
|
+
def allow_create?
|
31
|
+
@allow_create
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(rhs)
|
35
|
+
format == rhs.format && allow_create? == rhs.allow_create?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :id, :format
|
40
|
+
|
41
|
+
def self.from_xml(node)
|
42
|
+
node && new(node.content.strip, node['Format'])
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(id = nil, format = nil)
|
46
|
+
@id, @format = id, format
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(rhs)
|
50
|
+
id == rhs.id && format == rhs.format
|
51
|
+
end
|
52
|
+
|
53
|
+
def build(builder, options = {})
|
54
|
+
args = {}
|
55
|
+
args['Format'] = format if format
|
56
|
+
args['xmlns:saml'] = Namespaces::SAML if options[:include_namespace]
|
57
|
+
builder['saml'].__send__(options.delete(:element) || 'NameID', id, args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|