rsaml 0.1.2

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.
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