libsaml 2.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +91 -0
- data/Rakefile +33 -0
- data/lib/saml.rb +142 -0
- data/lib/saml/artifact.rb +51 -0
- data/lib/saml/artifact_resolve.rb +10 -0
- data/lib/saml/artifact_response.rb +9 -0
- data/lib/saml/assertion.rb +67 -0
- data/lib/saml/authn_request.rb +34 -0
- data/lib/saml/base.rb +47 -0
- data/lib/saml/bindings/http_artifact.rb +44 -0
- data/lib/saml/bindings/http_post.rb +29 -0
- data/lib/saml/bindings/http_redirect.rb +100 -0
- data/lib/saml/bindings/soap.rb +31 -0
- data/lib/saml/complex_types/endpoint_type.rb +17 -0
- data/lib/saml/complex_types/indexed_endpoint_type.rb +15 -0
- data/lib/saml/complex_types/request_abstract_type.rb +57 -0
- data/lib/saml/complex_types/sso_descriptor_type.rb +48 -0
- data/lib/saml/complex_types/status_response_type.rb +29 -0
- data/lib/saml/config.rb +49 -0
- data/lib/saml/elements/attribute.rb +24 -0
- data/lib/saml/elements/attribute_statement.rb +26 -0
- data/lib/saml/elements/audience_restriction.rb +12 -0
- data/lib/saml/elements/authn_context.rb +13 -0
- data/lib/saml/elements/authn_statement.rb +25 -0
- data/lib/saml/elements/conditions.rb +24 -0
- data/lib/saml/elements/contact_person.rb +33 -0
- data/lib/saml/elements/entities_descriptor.rb +27 -0
- data/lib/saml/elements/entity_descriptor.rb +37 -0
- data/lib/saml/elements/idp_sso_descriptor.rb +23 -0
- data/lib/saml/elements/key_descriptor.rb +34 -0
- data/lib/saml/elements/key_descriptor/key_info.rb +30 -0
- data/lib/saml/elements/key_descriptor/key_info/x509_data.rb +34 -0
- data/lib/saml/elements/name_id.rb +14 -0
- data/lib/saml/elements/organization.rb +16 -0
- data/lib/saml/elements/requested_authn_context.rb +28 -0
- data/lib/saml/elements/signature.rb +33 -0
- data/lib/saml/elements/signature/canonicalization_method.rb +19 -0
- data/lib/saml/elements/signature/digest_method.rb +19 -0
- data/lib/saml/elements/signature/inclusive_namespaces.rb +20 -0
- data/lib/saml/elements/signature/key_info.rb +14 -0
- data/lib/saml/elements/signature/reference.rb +23 -0
- data/lib/saml/elements/signature/signature_method.rb +19 -0
- data/lib/saml/elements/signature/signed_info.rb +24 -0
- data/lib/saml/elements/signature/transform.rb +19 -0
- data/lib/saml/elements/signature/transforms.rb +21 -0
- data/lib/saml/elements/sp_sso_descriptor.rb +27 -0
- data/lib/saml/elements/status.rb +15 -0
- data/lib/saml/elements/status_code.rb +42 -0
- data/lib/saml/elements/sub_status_code.rb +14 -0
- data/lib/saml/elements/subject.rb +38 -0
- data/lib/saml/elements/subject_confirmation.rb +30 -0
- data/lib/saml/elements/subject_confirmation_data.rb +23 -0
- data/lib/saml/elements/subject_locality.rb +12 -0
- data/lib/saml/encoding.rb +35 -0
- data/lib/saml/logout_request.rb +10 -0
- data/lib/saml/logout_response.rb +11 -0
- data/lib/saml/provider.rb +85 -0
- data/lib/saml/provider_stores/file.rb +33 -0
- data/lib/saml/response.rb +21 -0
- data/lib/saml/util.rb +51 -0
- data/lib/saml/version.rb +3 -0
- data/lib/saml/xml_helpers.rb +34 -0
- data/lib/tasks/saml_tasks.rake +4 -0
- metadata +195 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module Saml
|
2
|
+
class AuthnRequest
|
3
|
+
include Saml::ComplexTypes::RequestAbstractType
|
4
|
+
|
5
|
+
tag 'AuthnRequest'
|
6
|
+
attribute :force_authn, Boolean, :tag => "ForceAuthn"
|
7
|
+
attribute :assertion_consumer_service_index, Integer, :tag => "AssertionConsumerServiceIndex"
|
8
|
+
attribute :assertion_consumer_service_url, String, :tag => "AssertionConsumerServiceURL"
|
9
|
+
attribute :attribute_consuming_service_index, Integer, :tag => "AttributeConsumingServiceIndex"
|
10
|
+
attribute :protocol_binding, String, :tag => "ProtocolBinding"
|
11
|
+
attribute :provider_name, String, :tag => "ProviderName"
|
12
|
+
|
13
|
+
has_one :requested_authn_context, Saml::Elements::RequestedAuthnContext
|
14
|
+
|
15
|
+
validates :force_authn, :inclusion => [true, false, nil]
|
16
|
+
validates :assertion_consumer_service_index, :numericality => true, :if => "assertion_consumer_service_index.present?"
|
17
|
+
|
18
|
+
validate :check_assertion_consumer_service
|
19
|
+
|
20
|
+
def assertion_url
|
21
|
+
return assertion_consumer_service_url if assertion_consumer_service_url
|
22
|
+
provider.assertion_consumer_service_url(assertion_consumer_service_index) if assertion_consumer_service_index
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def check_assertion_consumer_service
|
28
|
+
if assertion_consumer_service_index.present?
|
29
|
+
errors.add(:assertion_consumer_service_url, :must_be_blank) if @assertion_consumer_service_url.present?
|
30
|
+
errors.add(:protocol_binding, :must_be_blank) if protocol_binding.present?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/saml/base.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'happymapper'
|
2
|
+
|
3
|
+
module Saml
|
4
|
+
module Base
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include ::HappyMapper
|
9
|
+
include ::ActiveModel::Validations
|
10
|
+
|
11
|
+
extend HappyMapperClassMethods
|
12
|
+
include HappyMapperInstanceMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module HappyMapperInstanceMethods
|
16
|
+
def initialize(attributes = {})
|
17
|
+
attributes.each do |key, value|
|
18
|
+
send("#{key}=", value) if respond_to?("#{key}=") && value.present?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_xml=(bool)
|
23
|
+
@from_xml = bool
|
24
|
+
end
|
25
|
+
|
26
|
+
def from_xml?
|
27
|
+
@from_xml
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module HappyMapperClassMethods
|
32
|
+
def parse(xml, options = {})
|
33
|
+
object = super
|
34
|
+
if object.is_a?(Array)
|
35
|
+
object.map { |x| x.from_xml = true }
|
36
|
+
elsif object
|
37
|
+
object.from_xml = true
|
38
|
+
end
|
39
|
+
object
|
40
|
+
rescue Nokogiri::XML::SyntaxError => e
|
41
|
+
raise Saml::Errors::UnparseableMessage.new(e.message)
|
42
|
+
rescue NoMethodError => e
|
43
|
+
raise Saml::Errors::UnparseableMessage.new(e.message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Saml
|
2
|
+
module Bindings
|
3
|
+
class HTTPArtifact
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# @param [Saml::ArtifactResponse] artifact_response
|
7
|
+
def create_response_xml(artifact_response)
|
8
|
+
Saml::Util.sign_xml(artifact_response, :soap)
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_url(location, artifact, options = {})
|
12
|
+
uri = URI.parse(location)
|
13
|
+
query = [uri.query, "SAMLart=#{CGI.escape(artifact.to_s)}"]
|
14
|
+
|
15
|
+
query << "RelayState=#{CGI.escape(options[:relay_state])}" if options[:relay_state]
|
16
|
+
|
17
|
+
uri.query = query.compact.join("&")
|
18
|
+
uri.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_message(request)
|
22
|
+
raw_xml = request.body.dup.read
|
23
|
+
artifact_resolve = Saml::ArtifactResolve.parse(raw_xml, single: true)
|
24
|
+
|
25
|
+
Saml::Util.verify_xml(artifact_resolve, raw_xml)
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve(request, location)
|
29
|
+
artifact = request.params["SAMLart"]
|
30
|
+
artifact_resolve = Saml::ArtifactResolve.new(artifact: artifact, destination: location)
|
31
|
+
|
32
|
+
response = Saml::Util.post(location, Saml::Util.sign_xml(artifact_resolve, :soap))
|
33
|
+
|
34
|
+
if response.code == 200
|
35
|
+
artifact_response = Saml::ArtifactResponse.parse(response.body, single: true)
|
36
|
+
verified_artifact_response = Saml::Util.verify_xml(artifact_response, response.body)
|
37
|
+
|
38
|
+
verified_artifact_response.response if artifact_response.success?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Saml
|
2
|
+
module Bindings
|
3
|
+
class HTTPPost
|
4
|
+
class << self
|
5
|
+
def create_form_attributes(message, options = {})
|
6
|
+
param = message.is_a?(Saml::ComplexTypes::StatusResponseType) ? "SAMLResponse" : "SAMLRequest"
|
7
|
+
|
8
|
+
xml = Saml::Util.sign_xml(message)
|
9
|
+
|
10
|
+
variables = {}
|
11
|
+
variables[param] = Saml::Encoding.encode_64(xml)
|
12
|
+
variables["RelayState"] = options[:relay_state] if options[:relay_state]
|
13
|
+
|
14
|
+
{
|
15
|
+
location: message.destination,
|
16
|
+
variables: variables
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive_message(request, type)
|
21
|
+
message = Saml::Encoding.decode_64(request.params["SAMLRequest"] || request.params["SAMLResponse"])
|
22
|
+
request_or_response = Saml.parse_message(message, type)
|
23
|
+
|
24
|
+
Saml::Util.verify_xml(request_or_response, message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Saml
|
2
|
+
module Bindings
|
3
|
+
class HTTPRedirect
|
4
|
+
class << self
|
5
|
+
def create_url(request_or_response, options = {})
|
6
|
+
options[:signature_algorithm] ||= 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
7
|
+
new(request_or_response, options).create_url
|
8
|
+
end
|
9
|
+
|
10
|
+
def receive_message(http_request, options = {})
|
11
|
+
options[:signature] = Saml::Encoding.decode_64(http_request.params["Signature"] || "")
|
12
|
+
options[:signature_algorithm] = http_request.params["SigAlg"]
|
13
|
+
options[:relay_state] = http_request.params["RelayState"]
|
14
|
+
|
15
|
+
request_or_response = parse_request_or_response(options.delete(:type), http_request.params)
|
16
|
+
|
17
|
+
redirect_binding = new(request_or_response, options)
|
18
|
+
query_string = URI.parse(http_request.url).query
|
19
|
+
|
20
|
+
redirect_binding.verify_signature(query_string) if request_or_response.provider.authn_requests_signed?
|
21
|
+
|
22
|
+
request_or_response
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def parse_request_or_response(type, params)
|
28
|
+
message = decode_message(params["SAMLRequest"])
|
29
|
+
|
30
|
+
Saml.parse_message(message, type)
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode_message(message)
|
34
|
+
Saml::Encoding.decode_gzip(Saml::Encoding.decode_64(message)).gsub("\n", "")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_accessor :request_or_response, :signature_algorithm, :relay_state, :signature
|
39
|
+
|
40
|
+
def initialize(request_or_response, options = {})
|
41
|
+
@request_or_response = request_or_response
|
42
|
+
@signature_algorithm = options[:signature_algorithm]
|
43
|
+
@relay_state = options[:relay_state]
|
44
|
+
@signature = options[:signature]
|
45
|
+
end
|
46
|
+
|
47
|
+
def verify_signature(query)
|
48
|
+
unless request_or_response.provider.verify(signature_algorithm, signature, parse_signature_params(query))
|
49
|
+
raise Saml::Errors::SignatureInvalid.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_url
|
54
|
+
[request_or_response.destination, signed_params].join("?")
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
|
60
|
+
def parse_signature_params(query)
|
61
|
+
params = {}
|
62
|
+
query.split(/[&;]/).each do |pairs|
|
63
|
+
key, value = pairs.split('=', 2)
|
64
|
+
params[key] = value
|
65
|
+
end
|
66
|
+
|
67
|
+
relay_state = params["RelayState"] ? "&RelayState=#{params['RelayState']}" : ""
|
68
|
+
"SAMLRequest=#{params['SAMLRequest']}#{relay_state}&SigAlg=#{params['SigAlg']}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def encoded_message
|
72
|
+
Saml::Encoding.encode_64(Saml::Encoding.encode_gzip(request_or_response.to_xml))
|
73
|
+
end
|
74
|
+
|
75
|
+
def encoded_params
|
76
|
+
params.collect do |key, value|
|
77
|
+
"#{key}=#{CGI.escape(value)}"
|
78
|
+
end.join('&')
|
79
|
+
end
|
80
|
+
|
81
|
+
def params
|
82
|
+
params = {}
|
83
|
+
|
84
|
+
params["SAMLRequest"] = encoded_message
|
85
|
+
params["RelayState"] = relay_state if relay_state
|
86
|
+
params["SigAlg"] = signature_algorithm if signature_algorithm
|
87
|
+
|
88
|
+
params
|
89
|
+
end
|
90
|
+
|
91
|
+
def signed_params
|
92
|
+
signature = request_or_response.provider.sign(signature_algorithm, encoded_params)
|
93
|
+
|
94
|
+
encoded_signature = CGI.escape(Saml::Encoding.encode_64(signature))
|
95
|
+
|
96
|
+
"#{encoded_params}&Signature=#{encoded_signature}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Saml
|
2
|
+
module Bindings
|
3
|
+
class SOAP
|
4
|
+
class << self
|
5
|
+
def create_response_xml(response)
|
6
|
+
Saml::Util.sign_xml(response, :soap)
|
7
|
+
end
|
8
|
+
|
9
|
+
def post_message(message, response_type)
|
10
|
+
signed_message = Saml::Util.sign_xml(message, :soap)
|
11
|
+
|
12
|
+
http_response = Saml::Util.post(message.destination, signed_message)
|
13
|
+
|
14
|
+
if http_response.code == 200
|
15
|
+
response = Saml.parse_message(http_response.body, response_type)
|
16
|
+
Saml::Util.verify_xml(response, http_response.body)
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def receive_message(request, type)
|
23
|
+
raw_xml = request.body.dup.read
|
24
|
+
message = Saml.parse_message(raw_xml, type)
|
25
|
+
|
26
|
+
Saml::Util.verify_xml(message, raw_xml)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Saml
|
2
|
+
module ComplexTypes
|
3
|
+
module EndpointType
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Saml::Base
|
6
|
+
|
7
|
+
included do
|
8
|
+
namespace 'md'
|
9
|
+
|
10
|
+
attribute :binding, String, :tag => "Binding"
|
11
|
+
attribute :location, String, :tag => "Location"
|
12
|
+
|
13
|
+
validates :binding, :location, :presence => true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Saml
|
2
|
+
module ComplexTypes
|
3
|
+
module IndexedEndpointType
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include EndpointType
|
6
|
+
|
7
|
+
included do
|
8
|
+
attribute :index, Integer, :tag => "index"
|
9
|
+
attribute :is_default, HappyMapper::Boolean, :tag => "isDefault"
|
10
|
+
|
11
|
+
validates :index, :presence => true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'happymapper'
|
2
|
+
|
3
|
+
module Saml
|
4
|
+
module ComplexTypes
|
5
|
+
module RequestAbstractType
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include Saml::Base
|
8
|
+
include Saml::XMLHelpers
|
9
|
+
|
10
|
+
included do
|
11
|
+
register_namespace 'samlp', Saml::SAMLP_NAMESPACE
|
12
|
+
register_namespace 'saml', Saml::SAML_NAMESPACE
|
13
|
+
namespace 'samlp'
|
14
|
+
|
15
|
+
attribute :_id, String, :tag => 'ID'
|
16
|
+
attribute :version, String, :tag => "Version"
|
17
|
+
attribute :issue_instant, Time, :tag => "IssueInstant", :on_save => lambda { |val| val.utc.xmlschema }
|
18
|
+
|
19
|
+
attribute :destination, String, :tag => "Destination"
|
20
|
+
element :issuer, String, :namespace => 'saml', :tag => "Issuer"
|
21
|
+
|
22
|
+
has_one :signature, Saml::Elements::Signature, :tag => "Signature"
|
23
|
+
|
24
|
+
validates :_id, :version, :issue_instant, :presence => true
|
25
|
+
|
26
|
+
validates :version, inclusion: %w(2.0)
|
27
|
+
validate :check_issue_instant, :if => "issue_instant.present?"
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(*args)
|
31
|
+
super(*args)
|
32
|
+
@_id ||= Saml.generate_id
|
33
|
+
@issue_instant ||= Time.now
|
34
|
+
@issuer ||= Saml::Config.entity_id
|
35
|
+
@version ||= Saml::SAML_VERSION
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_signature
|
39
|
+
self.signature = Saml::Elements::Signature.new(uri: "##{self._id}")
|
40
|
+
x509certificate = OpenSSL::X509::Certificate.new(provider.certificate) rescue nil
|
41
|
+
self.signature.key_info = Saml::Elements::KeyDescriptor::KeyInfo.new(x509certificate.to_pem) if x509certificate
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Saml::Provider]
|
45
|
+
def provider
|
46
|
+
Saml.provider(issuer)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def check_issue_instant
|
52
|
+
errors.add(:issue_instant, :too_old) if issue_instant < Time.now - Saml::Config.max_issue_instant_offset.minutes
|
53
|
+
errors.add(:issue_instant, :too_new) if issue_instant > Time.now + Saml::Config.max_issue_instant_offset.minutes
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Saml
|
2
|
+
module ComplexTypes
|
3
|
+
module SSODescriptorType
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Saml::Base
|
6
|
+
|
7
|
+
class ArtifactResolutionService
|
8
|
+
include Saml::ComplexTypes::IndexedEndpointType
|
9
|
+
|
10
|
+
tag 'ArtifactResolutionService'
|
11
|
+
namespace 'md'
|
12
|
+
end
|
13
|
+
|
14
|
+
class SingleLogoutService
|
15
|
+
include Saml::ComplexTypes::EndpointType
|
16
|
+
|
17
|
+
tag 'SingleLogoutService'
|
18
|
+
namespace 'md'
|
19
|
+
end
|
20
|
+
|
21
|
+
included do
|
22
|
+
namespace 'md'
|
23
|
+
|
24
|
+
PROTOCOL_SUPPORT_ENUMERATION = "urn:oasis:names:tc:SAML:2.0:protocol" unless defined?(PROTOCOL_SUPPORT_ENUMERATION)
|
25
|
+
|
26
|
+
attribute :protocol_support_enumeration, String, :tag => "protocolSupportEnumeration"
|
27
|
+
attribute :valid_until, Time, :tag => "validUntil"
|
28
|
+
attribute :cache_duration, Integer, :tag => "cacheDuration"
|
29
|
+
attribute :error_url, String, :tag => "errorURL"
|
30
|
+
|
31
|
+
has_many :key_descriptors, Saml::Elements::KeyDescriptor
|
32
|
+
|
33
|
+
has_many :artifact_resolution_services, ArtifactResolutionService
|
34
|
+
has_many :single_logout_services, SingleLogoutService
|
35
|
+
|
36
|
+
validates :protocol_support_enumeration, :presence => true, :inclusion => [PROTOCOL_SUPPORT_ENUMERATION]
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(*args)
|
40
|
+
super(*args)
|
41
|
+
@single_logout_services ||= []
|
42
|
+
@key_descriptors ||= []
|
43
|
+
@artifact_resolution_services ||= []
|
44
|
+
@protocol_support_enumeration ||= PROTOCOL_SUPPORT_ENUMERATION
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'happymapper'
|
2
|
+
|
3
|
+
module Saml
|
4
|
+
module ComplexTypes
|
5
|
+
module StatusResponseType
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include RequestAbstractType
|
9
|
+
|
10
|
+
included do
|
11
|
+
attribute :in_response_to, String, :tag => 'InResponseTo'
|
12
|
+
has_one :status, Saml::Elements::Status
|
13
|
+
|
14
|
+
validates :in_response_to, :status, :presence => true
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(*args)
|
18
|
+
options = args.extract_options!
|
19
|
+
@status = Saml::Elements::Status.new(:status_code => Saml::Elements::StatusCode.new(:value => options.delete(:status_value),
|
20
|
+
:sub_status_value => options.delete(:sub_status_value)))
|
21
|
+
super(*(args << options))
|
22
|
+
end
|
23
|
+
|
24
|
+
def success?
|
25
|
+
status.status_code.success?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|