rsaml 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/LICENSE +0 -0
  2. data/README +13 -0
  3. data/Rakefile +136 -0
  4. data/lib/rsaml.rb +57 -0
  5. data/lib/rsaml/action.rb +57 -0
  6. data/lib/rsaml/action_namespace.rb +63 -0
  7. data/lib/rsaml/advice.rb +34 -0
  8. data/lib/rsaml/assertion.rb +192 -0
  9. data/lib/rsaml/attribute.rb +76 -0
  10. data/lib/rsaml/audience.rb +19 -0
  11. data/lib/rsaml/authentication_context.rb +34 -0
  12. data/lib/rsaml/authn_context/README +1 -0
  13. data/lib/rsaml/authn_context/authentication_context_declaration.rb +42 -0
  14. data/lib/rsaml/authn_context/identification.rb +10 -0
  15. data/lib/rsaml/authn_context/physical_verification.rb +24 -0
  16. data/lib/rsaml/condition.rb +13 -0
  17. data/lib/rsaml/conditions.rb +107 -0
  18. data/lib/rsaml/encrypted.rb +12 -0
  19. data/lib/rsaml/errors.rb +16 -0
  20. data/lib/rsaml/evidence.rb +21 -0
  21. data/lib/rsaml/ext/string.rb +5 -0
  22. data/lib/rsaml/identifier.rb +9 -0
  23. data/lib/rsaml/identifier/base.rb +23 -0
  24. data/lib/rsaml/identifier/issuer.rb +28 -0
  25. data/lib/rsaml/identifier/name.rb +55 -0
  26. data/lib/rsaml/parser.rb +23 -0
  27. data/lib/rsaml/protocol.rb +21 -0
  28. data/lib/rsaml/protocol/artifact_resolve.rb +14 -0
  29. data/lib/rsaml/protocol/assertion_id_request.rb +18 -0
  30. data/lib/rsaml/protocol/authn_request.rb +91 -0
  31. data/lib/rsaml/protocol/idp_entry.rb +18 -0
  32. data/lib/rsaml/protocol/idp_list.rb +28 -0
  33. data/lib/rsaml/protocol/message.rb +65 -0
  34. data/lib/rsaml/protocol/name_id_policy.rb +31 -0
  35. data/lib/rsaml/protocol/query.rb +12 -0
  36. data/lib/rsaml/protocol/query/attribute_query.rb +56 -0
  37. data/lib/rsaml/protocol/query/authn_query.rb +30 -0
  38. data/lib/rsaml/protocol/query/authz_decision_query.rb +40 -0
  39. data/lib/rsaml/protocol/query/subject_query.rb +22 -0
  40. data/lib/rsaml/protocol/request.rb +27 -0
  41. data/lib/rsaml/protocol/requested_authn_context.rb +34 -0
  42. data/lib/rsaml/protocol/response.rb +56 -0
  43. data/lib/rsaml/protocol/scoping.rb +33 -0
  44. data/lib/rsaml/protocol/status.rb +38 -0
  45. data/lib/rsaml/protocol/status_code.rb +84 -0
  46. data/lib/rsaml/proxy_restriction.rb +30 -0
  47. data/lib/rsaml/statement.rb +10 -0
  48. data/lib/rsaml/statement/attribute_statement.rb +27 -0
  49. data/lib/rsaml/statement/authentication_statement.rb +57 -0
  50. data/lib/rsaml/statement/authorization_decision_statement.rb +53 -0
  51. data/lib/rsaml/statement/base.rb +9 -0
  52. data/lib/rsaml/subject.rb +37 -0
  53. data/lib/rsaml/subject_confirmation.rb +35 -0
  54. data/lib/rsaml/subject_confirmation_data.rb +55 -0
  55. data/lib/rsaml/subject_locality.rb +27 -0
  56. data/lib/rsaml/validatable.rb +21 -0
  57. data/lib/rsaml/version.rb +9 -0
  58. data/lib/xml_enc.rb +3 -0
  59. data/lib/xml_sig.rb +11 -0
  60. data/lib/xml_sig/canonicalization_method.rb +43 -0
  61. data/lib/xml_sig/key_info.rb +55 -0
  62. data/lib/xml_sig/reference.rb +57 -0
  63. data/lib/xml_sig/signature.rb +29 -0
  64. data/lib/xml_sig/signature_method.rb +20 -0
  65. data/lib/xml_sig/signed_info.rb +27 -0
  66. data/lib/xml_sig/transform.rb +37 -0
  67. data/test/action_namespace_test.rb +93 -0
  68. data/test/action_test.rb +51 -0
  69. data/test/advice_test.rb +25 -0
  70. data/test/assertion_test.rb +192 -0
  71. data/test/attribute_test.rb +60 -0
  72. data/test/authentication_context_test.rb +26 -0
  73. data/test/conditions_test.rb +84 -0
  74. data/test/evidence_test.rb +33 -0
  75. data/test/identifier_test.rb +22 -0
  76. data/test/issuer_test.rb +33 -0
  77. data/test/name_test.rb +33 -0
  78. data/test/parser_test.rb +32 -0
  79. data/test/protocol/assertion_id_request_test.rb +19 -0
  80. data/test/protocol/attribute_query_test.rb +30 -0
  81. data/test/protocol/authn_query_test.rb +20 -0
  82. data/test/protocol/authn_request_test.rb +56 -0
  83. data/test/protocol/authz_decision_query_test.rb +31 -0
  84. data/test/protocol/idp_list_test.rb +15 -0
  85. data/test/protocol/request_test.rb +66 -0
  86. data/test/protocol/response_test.rb +68 -0
  87. data/test/protocol/scoping_test.rb +20 -0
  88. data/test/protocol/status_code_test.rb +34 -0
  89. data/test/protocol/status_test.rb +16 -0
  90. data/test/proxy_restriction_test.rb +20 -0
  91. data/test/rsaml_test.rb +12 -0
  92. data/test/statement_test.rb +101 -0
  93. data/test/subject_locality_test.rb +27 -0
  94. data/test/subject_test.rb +44 -0
  95. data/test/test_helper.rb +16 -0
  96. data/test/xml_sig/canonicalization_test.rb +19 -0
  97. metadata +187 -0
@@ -0,0 +1,30 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ module Query #:nodoc:
4
+ # An AuthnQuery is used to make the query "What assertions containing authentication statements
5
+ # are available for this subject?" A successful response will contain one or more assertions containing
6
+ # authentication statements.
7
+ class AuthnQuery < SubjectQuery
8
+ # If present, specifies a filter for possible responses. Such a query asks the question "What assertions
9
+ # containing authentication statements do you have for this subject within the context of the supplied
10
+ # session information?" The value of this attribute MUST be a string.
11
+ attr_accessor :session_index
12
+
13
+ # If present, specifies a filter for possible responses. Such a query asks the question "What assertions
14
+ # containing authentication statements do you have for this subject that satisfy the authentication
15
+ # context requirements in this element?" The value of this attribute MUST be a RequestedAuthnContext
16
+ # instance.
17
+ attr_accessor :requested_authn_context
18
+
19
+ # Construct an XML fragment representing the authn query
20
+ def to_xml(xml=Builder::XmlMarkup.new)
21
+ attributes = {}
22
+ attributes['SessionIndex'] = session_index unless session_index.nil?
23
+ xml.tag!('samlp:AuthnQuery', attributes) {
24
+ xml << subject.to_xml unless subject.nil?
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ # Source code for the RSAML::Protocol::Query::AuthzDecisionQuery class.
2
+
3
+ module RSAML #:nodoc:
4
+ module Protocol #:nodoc:
5
+ module Query #:nodoc:
6
+ # Used to make the query "Should these actions on this resource be allowed for this subject,
7
+ # given this evidence?" A successful response will be in the form of assertions containing
8
+ # authorization decision statements.
9
+ class AuthzDecisionQuery < SubjectQuery
10
+ # A URI reference indicating the resource for which authorization is requested.
11
+ attr_accessor :resource
12
+
13
+ # The actions for which authorization is requested.
14
+ def actions
15
+ @actions ||= []
16
+ end
17
+
18
+ # A set of assertions that the SAML authority MAY rely on in making its authorization decision.
19
+ attr_accessor :evidence
20
+
21
+ # Validate the query structure.
22
+ def validate
23
+ raise ValidationError, "Resource is required" if resource.nil?
24
+ raise ValidationError, "At least one action is required" if actions.empty?
25
+ actions.each { |action| action.validate }
26
+ end
27
+
28
+ # Construct an XML fragment representing the authorization decision query
29
+ def to_xml(xml=Builder::XmlMarkup.new)
30
+ attributes = {'Resource' => resource}
31
+ xml.tag!('samlp:AuthzDecisionQuery', attributes) {
32
+ xml << subject.to_xml unless subject.nil?
33
+ actions.each { |action| xml << action.to_xml }
34
+ xml << evidence.to_xml unless evidence.nil?
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ module Query #:nodoc:
4
+ # Extension point that allows new SAML queries to be defined that specify a single SAML subject.
5
+ # This class should not be instantiated directly.
6
+ class SubjectQuery < RSAML::Protocol::Request
7
+ # The subject
8
+ attr_accessor :subject
9
+
10
+ # Initialize the subject query
11
+ def initialize(subject)
12
+ @subject = subject
13
+ end
14
+
15
+ # Validate the subject query structure.
16
+ def validate
17
+ raise ValidationError, "Subject is required" if subject.nil?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # A SAML request
4
+ class Request < Message
5
+ # Generate a Response instance with the given status code. The response's in_response_to attribute
6
+ # will be set to the ID of the request.
7
+ def respond(status)
8
+ response = Response.new(status)
9
+ response.in_response_to = id
10
+ response
11
+ end
12
+
13
+ # Construct an XML fragment representing the request
14
+ def to_xml(xml=Builder::XmlMarkup.new)
15
+ attributes = {'ID' => id, 'Version' => version, 'IssueInstant' => issue_instant.xmlschema}
16
+ attributes['Destination'] = destination unless destination.nil?
17
+ attributes['Consent'] = consent unless consent.nil?
18
+ attributes = add_xmlns(attributes)
19
+ xml.tag!('samlp:Request', attributes) {
20
+ xml << issuer.to_xml unless issuer.nil?
21
+ xml << signature.to_xml unless signature.nil?
22
+ # TODO: add extensions support
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # Specifies the authentication context requirements of authentication statements returned in
4
+ # response to a request or query.
5
+ class RequestedAuthnContext
6
+ # List of available comparison values
7
+ def self.comparisons
8
+ @comparisons ||= ['exact','minimum','maximum','better']
9
+ end
10
+
11
+ # Authentication context references, either AuthnContextDeclRef or AuthnContextClassRef.
12
+ def authn_context_refs
13
+ @authn_context_refs ||= []
14
+ end
15
+
16
+ # Specifies the comparison method used to evaluate the requested context classes or statements, one
17
+ # of "exact", "minimum", "maximum", or "better". The default is "exact"
18
+ def comparison
19
+ @comparison ||= 'exact'
20
+ end
21
+
22
+ # Validate the structure of the requested authn context
23
+ def validate
24
+ raise ValidationError, "Unknown comparison type: #{comparison}" unless RequestedAuthContext.comparisons.include?(comparison)
25
+ end
26
+
27
+ # Construct an XML fragment representing the requested authn context
28
+ def to_xml(xml=Builder::XmlMarkup.new)
29
+ attributes = {'Comparison' => comparison}
30
+ xml.tag!('samlp:RequestedAuthnContext')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # A SAML response
4
+ class Response < Message
5
+ # A reference to the identifier of the request to which the response corresponds, if any. If the response
6
+ # is not generated in response to a request, or if the ID attribute value of a request cannot be
7
+ # determined (for example, the request is malformed), then this attribute MUST NOT be present.
8
+ # Otherwise, it MUST be present and its value MUST match the value of the corresponding request's
9
+ # ID attribute.
10
+ attr_accessor :in_response_to
11
+
12
+ # A code representing the status of the corresponding request.
13
+ attr_accessor :status
14
+
15
+ # Initialize the Response instance
16
+ def initialize(status)
17
+ super()
18
+ @status = status
19
+ end
20
+
21
+ # SAML assertions
22
+ def assertions
23
+ @assertions ||= []
24
+ end
25
+
26
+ # SAML encrypted assertions
27
+ def encrypted_assertions
28
+ @encrypted_assertions ||= []
29
+ end
30
+
31
+ # Validate the request structure
32
+ def validate
33
+ super
34
+ raise ValidationError, "Status must be specified" if status.nil?
35
+ raise ValidationError, "Status must be a RSAML::Protocol::Status instance" unless status.is_a?(Status)
36
+ end
37
+
38
+ # Construct an XML fragment representing the request
39
+ def to_xml(xml=Builder::XmlMarkup.new)
40
+ attributes = {'ID' => id, 'Version' => version, 'IssueInstant' => issue_instant.xmlschema}
41
+ attributes['InResponseTo'] = in_response_to unless in_response_to.nil?
42
+ attributes['Destination'] = destination unless destination.nil?
43
+ attributes['Consent'] = consent unless consent.nil?
44
+ attributes = add_xmlns(attributes)
45
+ xml.tag!('samlp:Response', attributes) {
46
+ xml << issuer.to_xml unless issuer.nil?
47
+ xml << signature.to_xml unless signature.nil?
48
+ # TODO: add extensions support
49
+ xml << status.to_xml unless status.nil?
50
+ assertions.each { |assertion| xml << assertion.to_xml }
51
+ encrypted_assertions.each { |encrypted_assertion| xml << encrypted_assertion.to_xml }
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,33 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # specifies the identity providers trusted by the requester to authenticate the presenter, as well as
4
+ # limitations and context related to proxying of the <AuthnRequest> message to subsequent identity
5
+ # providers by the responder.
6
+ class Scoping
7
+ # Specifies the number of proxying indirections permissible between the identity provider that receives
8
+ # this <AuthnRequest> and the identity provider who ultimately authenticates the principal. A count of
9
+ # zero permits no proxying, while omitting this attribute expresses no such restriction.
10
+ attr_accessor :proxy_count
11
+
12
+ # An advisory list of identity providers and associated information that the requester deems acceptable
13
+ # to respond to the request.
14
+ attr_accessor :idp_list
15
+
16
+ # Identifies the set of requesting entities on whose behalf the requester is acting. Used to communicate
17
+ # the chain of requesters when proxying occurs.
18
+ def requestor_ids
19
+ @requestor_ids ||= []
20
+ end
21
+
22
+ # Construct an XML fragment representing the scoping
23
+ def to_xml(xml=Builder::XmlMarkup.new)
24
+ attributes = {}
25
+ attributes['ProxyCount'] = proxy_count if proxy_count
26
+ xml.tag!('samlp:Scoping', attributes) {
27
+ xml << idp_list.to_xml if idp_list
28
+ requestor_ids.each { |requestor_id| xml << requestor_id.to_xml }
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # A SAML status indicator.
4
+ class Status
5
+ # A code representing the status of the activity carried out in response to the corresponding request.
6
+ attr_accessor :status_code
7
+
8
+ # A message which MAY be returned to an operator.
9
+ attr_accessor :status_message
10
+
11
+ # Initialize the status with the given status code
12
+ def initialize(status_code)
13
+ @status_code = status_code
14
+ end
15
+
16
+ # Additional information concerning the status of the request. All objects in the collection must
17
+ # respond to the to_xml method.
18
+ def status_detail
19
+ @status_detail ||= []
20
+ end
21
+
22
+ # Validate the structure of the Status instance
23
+ def validate
24
+ raise ValidationError, "Status code required" if status_code.nil?
25
+ raise ValidationError, "Status code must be a RSAML::Protocol::StatusCode instance" unless status_code.is_a?(StatusCode)
26
+ end
27
+
28
+ # Construct an XML fragment representing the request
29
+ def to_xml(xml=Builder::XmlMarkup.new)
30
+ xml.tag!('samlp:Status') {
31
+ xml << status_code.to_xml unless status_code.nil?
32
+ xml.tag!('StatusMessage', status_message) unless status_message.nil?
33
+ status_detail.each { |status_detail| xml << status_detail.to_xml }
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,84 @@
1
+ module RSAML #:nodoc:
2
+ module Protocol #:nodoc:
3
+ # A code or a set of nested codes representing the status of the corresponding request.
4
+ #
5
+ # More information on available status codes may be found in Section 3.2.2.2 of the SAML 2.0 Core
6
+ # specification.
7
+ class StatusCode
8
+ # Initialize the status code with the given value
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ # Constant respresenting the Success status
14
+ SUCCESS = StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:Success')
15
+
16
+ # Constant representing the Requestor status
17
+ REQUESTOR = StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:Requestor')
18
+
19
+ # Constant representing the Responder status
20
+ RESPONDER = StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:Responder')
21
+
22
+ # Constant representing the VersionMismatch status
23
+ VERSION_MISMATCH = StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:VersionMismatch')
24
+
25
+ # Hash of symbol/StatusCode pairs representing top-level status codes.
26
+ def self.top_level_status_codes
27
+ @top_level_status_codes ||= {
28
+ :success => SUCCESS,
29
+ :requestor => REQUESTOR,
30
+ :responder => RESPONDER,
31
+ :version_mismatch => VERSION_MISMATCH
32
+ }
33
+ end
34
+
35
+ # Hash of symbol/StatusCode pairs representing second-level status codes.
36
+ def self.second_level_status_codes
37
+ @second_level_status_codes ||= {
38
+ :authn_failed => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:AuthnFailed'),
39
+ :invalid_attr_name_or_value => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue'),
40
+ :invalid_name_id_policy => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'),
41
+ :no_authn_context => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext'),
42
+ :no_available_idp => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP'),
43
+ :no_passive => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:NoPassive'),
44
+ :no_supported_idp => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP'),
45
+ :partial_logout => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:PartialLogout'),
46
+ :proxy_count_exceeded => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded'),
47
+ :request_denied => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:RequestDenied'),
48
+ :request_unsupported => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported'),
49
+ :request_version_deprecated => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated'),
50
+ :request_version_too_high => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh'),
51
+ :request_version_too_low => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow'),
52
+ :resource_not_recognized => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized'),
53
+ :too_many_responses => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:TooManyResponse'),
54
+ :unknown_attr_profile => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile'),
55
+ :unknown_principal => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal'),
56
+ :unsupported_binding => StatusCode.new('urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding'),
57
+ }
58
+ end
59
+
60
+ # The status code value. Value is a URI reference.
61
+ attr_accessor :value
62
+
63
+ # An optional child status code.
64
+ attr_accessor :status_code
65
+
66
+ def validate
67
+ raise ValidationError, "Value is required" if value.nil?
68
+ end
69
+
70
+ # Construct an XML fragment representing the request
71
+ def to_xml(xml=Builder::XmlMarkup.new)
72
+ attributes = {'Value' => value}
73
+ xml.tag!('samlp:StatusCode', attributes) {
74
+ xml << status_code.to_xml unless status_code.nil?
75
+ }
76
+ end
77
+
78
+ # Return the value of the status code as a string.
79
+ def to_s
80
+ value
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ module RSAML #:nodoc:
2
+ # Specifies limitations that the asserting party imposes on relying parties that in turn wish to act as asserting
3
+ # parties and issue subsequent assertions of their own on the basis of the information contained in the
4
+ # original assertion. A relying party acting as an asserting party MUST NOT issue an assertion that itself
5
+ # violates the restrictions specified in this condition on the basis of an assertion containing such a condition.
6
+ class ProxyRestriction
7
+ # Specifies the maximum number of indirections that the asserting party permits to exist between this
8
+ # assertion and an assertion which has ultimately been issued on the basis of it.
9
+ attr_accessor :count
10
+
11
+ def audiences
12
+ @audiences ||= []
13
+ end
14
+
15
+ # Validate the structure
16
+ def validate
17
+ raise ValidationError, "Count must be 0 or more if specified" if !count.nil? && count < 0
18
+ end
19
+
20
+ # Construct an XML fragment representing the proxy restriction
21
+ def to_xml(xml=Builder::XmlMarkup.new)
22
+ attributes = {}
23
+ attributes['Count'] = count unless count.nil?
24
+ xml.tag!('saml:ProxyRestriction', attributes) {
25
+ audiences.each { |audience| xml << audience.to_xml }
26
+ }
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ module RSAML #:nodoc
2
+ # Module that contain SAML statements.
3
+ module Statement
4
+ end
5
+ end
6
+
7
+ require 'rsaml/statement/base'
8
+ require 'rsaml/statement/authentication_statement'
9
+ require 'rsaml/statement/attribute_statement'
10
+ require 'rsaml/statement/authorization_decision_statement'
@@ -0,0 +1,27 @@
1
+ module RSAML #:nodoc:
2
+ module Statement #:nodoc:
3
+ # The assertion subject is associated with the supplied attributes.
4
+ class AttributeStatement < Base
5
+ # Specifies attributes of the assertion subject.
6
+ def attributes
7
+ @attributes ||= []
8
+ end
9
+
10
+ # Validate the structure of the attribute statement. Raises a validation error if:
11
+ #
12
+ # * Has no attributes specified
13
+ # * Any of the attributes are invalid
14
+ def validate
15
+ raise ValidationError, "At least one attribute must be specified" if @attributes.empty?
16
+ @attributes.each { |attribute| attribute.validate }
17
+ end
18
+
19
+ # Construct an XML fragment representing the authentication statement
20
+ def to_xml(xml=Builder::XmlMarkup.new)
21
+ xml.tag!('saml:AttributeStatement') {
22
+ attributes.each { |attribute| xml << attribute.to_xml }
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end