googleauth 1.14.0 → 1.17.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/Credentials.md +110 -0
  4. data/Errors.md +152 -0
  5. data/README.md +0 -1
  6. data/lib/googleauth/api_key.rb +9 -0
  7. data/lib/googleauth/application_default.rb +3 -1
  8. data/lib/googleauth/base_client.rb +5 -0
  9. data/lib/googleauth/bearer_token.rb +16 -2
  10. data/lib/googleauth/client_id.rb +15 -7
  11. data/lib/googleauth/compute_engine.rb +64 -18
  12. data/lib/googleauth/credentials.rb +66 -35
  13. data/lib/googleauth/credentials_loader.rb +30 -5
  14. data/lib/googleauth/default_credentials.rb +69 -34
  15. data/lib/googleauth/errors.rb +117 -0
  16. data/lib/googleauth/external_account/aws_credentials.rb +87 -19
  17. data/lib/googleauth/external_account/base_credentials.rb +34 -4
  18. data/lib/googleauth/external_account/external_account_utils.rb +17 -5
  19. data/lib/googleauth/external_account/identity_pool_credentials.rb +42 -16
  20. data/lib/googleauth/external_account/pluggable_credentials.rb +37 -21
  21. data/lib/googleauth/external_account.rb +53 -7
  22. data/lib/googleauth/iam.rb +20 -4
  23. data/lib/googleauth/id_tokens/errors.rb +13 -7
  24. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  25. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  26. data/lib/googleauth/id_tokens.rb +4 -4
  27. data/lib/googleauth/impersonated_service_account.rb +119 -19
  28. data/lib/googleauth/json_key_reader.rb +17 -3
  29. data/lib/googleauth/oauth2/sts_client.rb +12 -6
  30. data/lib/googleauth/scope_util.rb +2 -2
  31. data/lib/googleauth/service_account.rb +55 -21
  32. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  33. data/lib/googleauth/signet.rb +30 -10
  34. data/lib/googleauth/user_authorizer.rb +38 -10
  35. data/lib/googleauth/user_refresh.rb +54 -19
  36. data/lib/googleauth/version.rb +1 -1
  37. data/lib/googleauth/web_user_authorizer.rb +54 -17
  38. data/lib/googleauth.rb +1 -0
  39. metadata +22 -19
@@ -14,9 +14,11 @@
14
14
 
15
15
  require "forwardable"
16
16
  require "json"
17
+ require "pathname"
17
18
  require "signet/oauth_2/client"
18
19
 
19
20
  require "googleauth/credentials_loader"
21
+ require "googleauth/errors"
20
22
 
21
23
  module Google
22
24
  module Auth
@@ -357,12 +359,13 @@ module Google
357
359
  # Creates a new Credentials instance with the provided auth credentials, and with the default
358
360
  # values configured on the class.
359
361
  #
360
- # @param [String, Hash, Signet::OAuth2::Client] source_creds
362
+ # @param [String, Pathname, Hash, Google::Auth::BaseClient] source_creds
361
363
  # The source of credentials. It can be provided as one of the following:
362
364
  #
363
- # * The path to a JSON keyfile (as a `String`)
365
+ # * The path to a JSON keyfile (as a `String` or a `Pathname`)
364
366
  # * The contents of a JSON keyfile (as a `Hash`)
365
- # * A `Signet::OAuth2::Client` credentials object
367
+ # * A `Google::Auth::BaseClient` credentials object, including but not limited to
368
+ # a `Signet::OAuth2::Client` object.
366
369
  # * Any credentials object that supports the methods this wrapper delegates to an inner client.
367
370
  #
368
371
  # If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
@@ -391,14 +394,20 @@ module Google
391
394
  # parameters of the `Signet::OAuth2::Client`, such as connection parameters,
392
395
  # timeouts, etc.
393
396
  #
397
+ # @raise [Google::Auth::InitializationError] If source_creds is nil
398
+ # @raise [ArgumentError] If both scope and target_audience are specified
399
+ #
394
400
  def initialize source_creds, options = {}
395
- raise "The source credentials passed to Google::Auth::Credentials.new were nil." if source_creds.nil?
401
+ if source_creds.nil?
402
+ raise InitializationError,
403
+ "The source credentials passed to Google::Auth::Credentials.new were nil."
404
+ end
396
405
 
397
406
  options = symbolize_hash_keys options
398
407
  @project_id = options[:project_id] || options[:project]
399
408
  @quota_project_id = options[:quota_project_id]
400
409
  case source_creds
401
- when String
410
+ when String, Pathname
402
411
  update_from_filepath source_creds, options
403
412
  when Hash
404
413
  update_from_hash source_creds, options
@@ -499,17 +508,61 @@ module Google
499
508
  token_credential_uri: options[:token_credential_uri] || token_credential_uri,
500
509
  audience: options[:audience] || audience
501
510
  }
502
- client = Google::Auth::DefaultCredentials.make_creds creds_input
511
+
512
+ # Determine the class, which consumes the IO stream
513
+ json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class creds_input[:json_key_io]
514
+
515
+ # Re-serialize the parsed JSON and replace the IO stream in creds_input
516
+ creds_input[:json_key_io] = StringIO.new JSON.generate(json_key)
517
+
518
+ client = clz.make_creds creds_input
503
519
  options = options.select { |k, _v| k == :logger }
504
520
  new client, options
505
521
  end
506
522
 
523
+ # @private
524
+ # Initializes the Signet client.
525
+ def self.init_client hash, options = {}
526
+ options = update_client_options options
527
+ io = StringIO.new JSON.generate hash
528
+
529
+ # Determine the class, which consumes the IO stream
530
+ json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class io
531
+
532
+ # Re-serialize the parsed JSON and create a new IO stream.
533
+ new_io = StringIO.new JSON.generate(json_key)
534
+
535
+ clz.make_creds options.merge!(json_key_io: new_io)
536
+ end
537
+
538
+ # @private
539
+ # Updates client options with defaults from the credential class
540
+ #
541
+ # @param [Hash] options Options to update
542
+ # @return [Hash] Updated options hash
543
+ # @raise [ArgumentError] If both scope and target_audience are specified
544
+ def self.update_client_options options
545
+ options = options.dup
546
+
547
+ # options have higher priority over constructor defaults
548
+ options[:token_credential_uri] ||= token_credential_uri
549
+ options[:audience] ||= audience
550
+ options[:scope] ||= scope
551
+ options[:target_audience] ||= target_audience
552
+
553
+ if !Array(options[:scope]).empty? && options[:target_audience]
554
+ raise ArgumentError, "Cannot specify both scope and target_audience"
555
+ end
556
+ options.delete :scope unless options[:target_audience].nil?
557
+
558
+ options
559
+ end
560
+
507
561
  private_class_method :from_env_vars,
508
562
  :from_default_paths,
509
563
  :from_application_default,
510
564
  :from_io
511
565
 
512
-
513
566
  # Creates a duplicate of these credentials. This method tries to create the duplicate of the
514
567
  # wrapped credentials if they support duplication and use them as is if they don't.
515
568
  #
@@ -554,17 +607,12 @@ module Google
554
607
  protected
555
608
 
556
609
  # Verify that the keyfile argument is a file.
610
+ #
611
+ # @param [String] keyfile Path to the keyfile
612
+ # @raise [Google::Auth::InitializationError] If the keyfile does not exist
557
613
  def verify_keyfile_exists! keyfile
558
614
  exists = ::File.file? keyfile
559
- raise "The keyfile '#{keyfile}' is not a valid file." unless exists
560
- end
561
-
562
- # Initializes the Signet client.
563
- def init_client hash, options = {}
564
- options = update_client_options options
565
- io = StringIO.new JSON.generate hash
566
- options.merge! json_key_io: io
567
- Google::Auth::DefaultCredentials.make_creds options
615
+ raise InitializationError, "The keyfile '#{keyfile}' is not a valid file." unless exists
568
616
  end
569
617
 
570
618
  # returns a new Hash with string keys instead of symbol keys.
@@ -577,23 +625,6 @@ module Google
577
625
  hash.to_h.transform_keys(&:to_sym)
578
626
  end
579
627
 
580
- def update_client_options options
581
- options = options.dup
582
-
583
- # options have higher priority over constructor defaults
584
- options[:token_credential_uri] ||= self.class.token_credential_uri
585
- options[:audience] ||= self.class.audience
586
- options[:scope] ||= self.class.scope
587
- options[:target_audience] ||= self.class.target_audience
588
-
589
- if !Array(options[:scope]).empty? && options[:target_audience]
590
- raise ArgumentError, "Cannot specify both scope and target_audience"
591
- end
592
- options.delete :scope unless options[:target_audience].nil?
593
-
594
- options
595
- end
596
-
597
628
  def update_from_client client
598
629
  @project_id ||= client.project_id if client.respond_to? :project_id
599
630
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
@@ -607,7 +638,7 @@ module Google
607
638
  hash["target_audience"] ||= options[:target_audience]
608
639
  @project_id ||= hash["project_id"] || hash["project"]
609
640
  @quota_project_id ||= hash["quota_project_id"]
610
- @client = init_client hash, options
641
+ @client = self.class.init_client hash, options
611
642
  end
612
643
 
613
644
  def update_from_filepath path, options
@@ -617,7 +648,7 @@ module Google
617
648
  json["target_audience"] ||= options[:target_audience]
618
649
  @project_id ||= json["project_id"] || json["project"]
619
650
  @quota_project_id ||= json["quota_project_id"]
620
- @client = init_client json, options
651
+ @client = self.class.init_client json, options
621
652
  end
622
653
 
623
654
  def setup_logging logger: :default
@@ -14,6 +14,9 @@
14
14
 
15
15
  require "os"
16
16
  require "rbconfig"
17
+ require "json"
18
+
19
+ require "googleauth/errors"
17
20
 
18
21
  module Google
19
22
  # Module Auth provides classes that provide Google-specific authorization
@@ -71,11 +74,12 @@ module Google
71
74
  # The following keys are recognized:
72
75
  # * `:default_connection` The connection object to use.
73
76
  # * `:connection_builder` A `Proc` that returns a connection.
77
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
74
78
  def from_env scope = nil, options = {}
75
79
  options = interpret_options scope, options
76
80
  if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
77
81
  path = ENV[ENV_VAR]
78
- raise "file #{path} does not exist" unless File.exist? path
82
+ raise InitializationError, "file #{path} does not exist" unless File.exist? path
79
83
  File.open path do |f|
80
84
  return make_creds options.merge(json_key_io: f)
81
85
  end
@@ -83,7 +87,7 @@ module Google
83
87
  make_creds options
84
88
  end
85
89
  rescue StandardError => e
86
- raise "#{NOT_FOUND_ERROR}: #{e}"
90
+ raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}"
87
91
  end
88
92
 
89
93
  # Creates an instance from a well known path.
@@ -97,6 +101,7 @@ module Google
97
101
  # The following keys are recognized:
98
102
  # * `:default_connection` The connection object to use.
99
103
  # * `:connection_builder` A `Proc` that returns a connection.
104
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
100
105
  def from_well_known_path scope = nil, options = {}
101
106
  options = interpret_options scope, options
102
107
  home_var = OS.windows? ? "APPDATA" : "HOME"
@@ -109,7 +114,7 @@ module Google
109
114
  return make_creds options.merge(json_key_io: f)
110
115
  end
111
116
  rescue StandardError => e
112
- raise "#{WELL_KNOWN_ERROR}: #{e}"
117
+ raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}"
113
118
  end
114
119
 
115
120
  # Creates an instance from the system default path
@@ -123,6 +128,7 @@ module Google
123
128
  # The following keys are recognized:
124
129
  # * `:default_connection` The connection object to use.
125
130
  # * `:connection_builder` A `Proc` that returns a connection.
131
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid
126
132
  def from_system_default_path scope = nil, options = {}
127
133
  options = interpret_options scope, options
128
134
  if OS.windows?
@@ -137,7 +143,7 @@ module Google
137
143
  return make_creds options.merge(json_key_io: f)
138
144
  end
139
145
  rescue StandardError => e
140
- raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
146
+ raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
141
147
  end
142
148
 
143
149
  module_function
@@ -147,12 +153,31 @@ module Google
147
153
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
148
154
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
149
155
  gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read)
150
- config = MultiJson.load gcloud_json
156
+ config = JSON.parse gcloud_json
151
157
  config["configuration"]["properties"]["core"]["project"]
152
158
  rescue StandardError
153
159
  nil
154
160
  end
155
161
 
162
+ # @private
163
+ # Loads a JSON key from an IO object, verifies its type, and rewinds the IO.
164
+ #
165
+ # @param json_key_io [IO] An IO object containing the JSON key.
166
+ # @param expected_type [String] The expected credential type name.
167
+ # @raise [Google::Auth::InitializationError] If the JSON key type does not match the expected type.
168
+ def load_and_verify_json_key_type json_key_io, expected_type
169
+ begin
170
+ json_key = JSON.parse json_key_io.read
171
+ rescue JSON::ParserError => e
172
+ raise Google::Auth::InitializationError, "Invalid JSON keyfile format: #{e.message}"
173
+ end
174
+ json_key_io.rewind # Rewind the stream so it can be read again.
175
+ return if json_key["type"] == expected_type
176
+ raise Google::Auth::InitializationError,
177
+ "The provided credentials were not of type '#{expected_type}'. " \
178
+ "Instead, the type was '#{json_key['type']}'."
179
+ end
180
+
156
181
  private
157
182
 
158
183
  def interpret_options scope, options
@@ -12,14 +12,16 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "multi_json"
15
+ require "json"
16
16
  require "stringio"
17
17
 
18
18
  require "googleauth/credentials_loader"
19
+ require "googleauth/errors"
19
20
  require "googleauth/external_account"
20
21
  require "googleauth/service_account"
21
22
  require "googleauth/service_account_jwt_header"
22
23
  require "googleauth/user_refresh"
24
+ require "googleauth/impersonated_service_account"
23
25
 
24
26
  module Google
25
27
  # Module Auth provides classes that provide Google-specific authorization
@@ -42,50 +44,83 @@ module Google
42
44
  # information, refer to [Validate credential configurations from external
43
45
  # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
44
46
  #
47
+ # @deprecated This method is deprecated and will be removed in a future version.
48
+ # Please use the `make_creds` method on the specific credential class you intend to load,
49
+ # e.g., `Google::Auth::ServiceAccountCredentials.make_creds`.
50
+ #
51
+ # This method does not validate the credential configuration. The security
52
+ # risk occurs when a credential configuration is accepted from a source that
53
+ # is not under your control and used without validation on your side.
54
+ #
55
+ # If you know that you will be loading credential configurations of a
56
+ # specific type, it is recommended to use a credential-type-specific
57
+ # `make_creds` method.
58
+ # This will ensure that an unexpected credential type with potential for
59
+ # malicious intent is not loaded unintentionally. You might still have to do
60
+ # validation for certain credential types. Please follow the recommendation
61
+ # for that method. For example, if you want to load only service accounts,
62
+ # you can use:
63
+ # ```
64
+ # creds = Google::Auth::ServiceAccountCredentials.make_creds
65
+ # ```
66
+ # @see Google::Auth::ServiceAccountCredentials.make_creds
67
+ #
68
+ # If you are loading your credential configuration from an untrusted source and have
69
+ # not mitigated the risks (e.g. by validating the configuration yourself), make
70
+ # these changes as soon as possible to prevent security risks to your environment.
71
+ #
72
+ # Regardless of the method used, it is always your responsibility to validate
73
+ # configurations received from external sources.
74
+ #
75
+ # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials for more details.
76
+ #
77
+ # @param options [Hash] Options for creating the credentials
78
+ # @return [Google::Auth::Credentials] The credentials instance
79
+ # @raise [Google::Auth::InitializationError] If the credentials cannot be determined
45
80
  def self.make_creds options = {}
46
81
  json_key_io = options[:json_key_io]
47
- if json_key_io
48
- json_key, clz = determine_creds_class json_key_io
49
- io = StringIO.new MultiJson.dump(json_key)
82
+ json_key, clz = determine_creds_class json_key_io
83
+ if json_key
84
+ io = StringIO.new JSON.generate(json_key)
50
85
  clz.make_creds options.merge(json_key_io: io)
51
86
  else
52
- clz = read_creds
53
87
  clz.make_creds options
54
88
  end
55
89
  end
56
90
 
57
- def self.read_creds
58
- env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
59
- type = ENV[env_var]
60
- raise "#{env_var} is undefined in env" unless type
61
- case type
62
- when "service_account"
63
- ServiceAccountCredentials
64
- when "authorized_user"
65
- UserRefreshCredentials
66
- when "external_account"
67
- ExternalAccount::Credentials
68
- else
69
- raise "credentials type '#{type}' is not supported"
70
- end
71
- end
72
-
73
91
  # Reads the input json and determines which creds class to use.
74
- def self.determine_creds_class json_key_io
75
- json_key = MultiJson.load json_key_io.read
76
- key = "type"
77
- raise "the json is missing the '#{key}' field" unless json_key.key? key
78
- type = json_key[key]
79
- case type
80
- when "service_account"
81
- [json_key, ServiceAccountCredentials]
82
- when "authorized_user"
83
- [json_key, UserRefreshCredentials]
84
- when "external_account"
85
- [json_key, ExternalAccount::Credentials]
92
+ #
93
+ # @param json_key_io [IO, nil] An optional IO object containing the JSON key.
94
+ # If nil, the credential type is determined from environment variables.
95
+ # @return [Array(Hash, Class)] The JSON key (or nil if from environment) and the credential class to use
96
+ # @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type,
97
+ # or if the environment variable is undefined or unsupported.
98
+ def self.determine_creds_class json_key_io = nil
99
+ if json_key_io
100
+ json_key = JSON.parse json_key_io.read
101
+ key = "type"
102
+ raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
103
+ type = json_key[key]
86
104
  else
87
- raise "credentials type '#{type}' is not supported"
105
+ env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
106
+ type = ENV[env_var]
107
+ raise InitializationError, "#{env_var} is undefined in env" unless type
108
+ json_key = nil
88
109
  end
110
+
111
+ clz = case type
112
+ when ServiceAccountCredentials::CREDENTIAL_TYPE_NAME
113
+ ServiceAccountCredentials
114
+ when UserRefreshCredentials::CREDENTIAL_TYPE_NAME
115
+ UserRefreshCredentials
116
+ when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME
117
+ ExternalAccount::Credentials
118
+ when ImpersonatedServiceAccountCredentials::CREDENTIAL_TYPE_NAME
119
+ ImpersonatedServiceAccountCredentials
120
+ else
121
+ raise InitializationError, "credentials type '#{type}' is not supported"
122
+ end
123
+ [json_key, clz]
89
124
  end
90
125
  end
91
126
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "signet/oauth_2/client"
4
+
5
+ # Copyright 2025 Google LLC
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Google
20
+ module Auth
21
+ ##
22
+ # Error mixin module for Google Auth errors
23
+ # All Google Auth errors should include this module
24
+ #
25
+ module Error; end
26
+
27
+ ##
28
+ # Mixin module that contains detailed error information
29
+ # typically this is available if credentials initialization
30
+ # succeeds and credentials object is valid
31
+ #
32
+ module DetailedError
33
+ include Error
34
+
35
+ # The type of the credentials that the error was originated from
36
+ # @return [String, nil] The class name of the credential that raised the error
37
+ attr_reader :credential_type_name
38
+
39
+ # The principal for the authentication flow. Typically obtained from credentials
40
+ # @return [String, Symbol, nil] The principal identifier associated with the credentials
41
+ attr_reader :principal
42
+
43
+ # All details passed in the options hash when creating the error
44
+ # @return [Hash] Additional details about the error
45
+ attr_reader :details
46
+
47
+ # @private
48
+ def self.included base
49
+ base.extend ClassMethods
50
+ end
51
+
52
+ # Class methods to be added to including classes
53
+ module ClassMethods
54
+ # Creates a new error with detailed information
55
+ # @param message [String] The error message
56
+ # @param credential_type_name [String] The credential type that raised the error
57
+ # @param principal [String, Symbol] The principal for the authentication flow
58
+ # @return [Error] The new error with details
59
+ def with_details message, credential_type_name:, principal:
60
+ new(message).tap do |error|
61
+ error.instance_variable_set :@credential_type_name, credential_type_name
62
+ error.instance_variable_set :@principal, principal
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Error raised during Credentials initialization.
70
+ # All new code should use this instead of ArgumentError during initializtion.
71
+ #
72
+ class InitializationError < StandardError
73
+ include Error
74
+ end
75
+
76
+ ##
77
+ # Generic error raised during operation of Credentials
78
+ # This should be used for all purposes not covered by other errors.
79
+ #
80
+ class CredentialsError < StandardError
81
+ include DetailedError
82
+ end
83
+
84
+ ##
85
+ # An error indicating the remote server refused to authorize the client.
86
+ # Maintains backward compatibility with Signet.
87
+ #
88
+ # Should not be used in the new code, even when wrapping `Signet::AuthorizationError`.
89
+ # New code should use CredentialsError instead.
90
+ #
91
+ class AuthorizationError < Signet::AuthorizationError
92
+ include DetailedError
93
+ end
94
+
95
+ ##
96
+ # An error indicating that the server sent an unexpected http status.
97
+ # Maintains backward compatibility with Signet.
98
+ #
99
+ # Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`.
100
+ # New code should use CredentialsError instead.
101
+ #
102
+ class UnexpectedStatusError < Signet::UnexpectedStatusError
103
+ include DetailedError
104
+ end
105
+
106
+ ##
107
+ # An error indicating the client failed to parse a value.
108
+ # Maintains backward compatibility with Signet.
109
+ #
110
+ # Should not be used in the new code, even when wrapping `Signet::ParseError`.
111
+ # New code should use CredentialsError instead.
112
+ #
113
+ class ParseError < Signet::ParseError
114
+ include DetailedError
115
+ end
116
+ end
117
+ end