googleauth 1.8.1 → 1.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37795e56189392a97d5941b4982730ffa2ee1dd53ec63593bbb7f5ad50be044e
4
- data.tar.gz: 72d6b584c89c321698485bda1e4aef33f361ed35da4762bd22b5dc4634556f8f
3
+ metadata.gz: 9e2eef22c1062f2fd2216760e90a1af9ece1b99838bccea486a7b4ce43be9734
4
+ data.tar.gz: 1e60ef4856de1e4f7a3d423f3953e5e39f86c7002216ad2d98ee46ec88aaaf25
5
5
  SHA512:
6
- metadata.gz: 1dcfc8f1e8e65f9b4b27c4933e71faffd2998d11766307cf464d0551d5f37e41c266e840a340a9aba41d49f44d05005006077526851b1b33e277ce3155d16370
7
- data.tar.gz: f036b3403998c93f41eae33c472ad714758e6c6d06e9a1b197b53d118d182c2abb9caf40636ff63c152e2186642430de824e7929dcd2978cd358a5bd03194c1c
6
+ metadata.gz: d5a87f962d04eb59c9ae083a4d3c46fd54ef4d4c53f0571b3952f6d1a9d30e3af89fb2c50e04b118e301097850c9985c713968dc29c62c5d388d0d8a4c3d306d
7
+ data.tar.gz: fae67434766ed2d4bb67e2c7ba2784a9397843110c28d96368d368b1dd30229a1ea83c5ee05e70a712446781e67ca84149fbaa72dd4239eb349d6943ff42b9b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # Release History
2
2
 
3
+ ### 1.14.0 (2025-03-14)
4
+
5
+ #### Features
6
+
7
+ * add API key credentials ([#520](https://github.com/googleapis/google-auth-library-ruby/issues/520))
8
+ * Add Bearer token credentials
9
+ * add BearerToken credentials ([#522](https://github.com/googleapis/google-auth-library-ruby/issues/522))
10
+ * Update minimum Ruby version to 3.0 ([#527](https://github.com/googleapis/google-auth-library-ruby/issues/527))
11
+ #### Bug Fixes
12
+
13
+ * Eliminated the "attribute accessor as module_function" warning ([#519](https://github.com/googleapis/google-auth-library-ruby/issues/519))
14
+ * Get the project_id from gcloud ([#479](https://github.com/googleapis/google-auth-library-ruby/issues/479))
15
+ * logger configuration in service account JWT header ([#525](https://github.com/googleapis/google-auth-library-ruby/issues/525))
16
+
17
+ ### 1.13.1 (2025-01-24)
18
+
19
+ #### Bug Fixes
20
+
21
+ * Signet client subclasses no longer make the update! method private ([#516](https://github.com/googleapis/google-auth-library-ruby/issues/516))
22
+
23
+ ### 1.13.0 (2025-01-22)
24
+
25
+ #### Features
26
+
27
+ * create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499))
28
+ #### Documentation
29
+
30
+ * Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512))
31
+
32
+ ### 1.12.2 (2024-12-19)
33
+
34
+ #### Bug Fixes
35
+
36
+ * GCECredentials lazily fetches from the metadata server to ensure a universe domain is known ([#509](https://github.com/googleapis/google-auth-library-ruby/issues/509))
37
+
38
+ ### 1.12.1 (2024-12-17)
39
+
40
+ #### Bug Fixes
41
+
42
+ * Restored previous behavior where the apply! method returns the auth header ([#506](https://github.com/googleapis/google-auth-library-ruby/issues/506))
43
+
44
+ ### 1.12.0 (2024-12-05)
45
+
46
+ #### Features
47
+
48
+ * provided opt-in debug logging ([#490](https://github.com/googleapis/google-auth-library-ruby/issues/490))
49
+
50
+ ### 1.11.2 (2024-10-23)
51
+
52
+ #### Bug Fixes
53
+
54
+ * Temporarily disable universe domain query from GCE metadata server ([#493](https://github.com/googleapis/google-auth-library-ruby/issues/493))
55
+ * Use updated metadata path for universe-domain ([#496](https://github.com/googleapis/google-auth-library-ruby/issues/496))
56
+
57
+ ### 1.11.1 (2024-10-04)
58
+
59
+ #### Bug Fixes
60
+
61
+ * Fixed parsing of expiration timestamp from ID tokens ([#492](https://github.com/googleapis/google-auth-library-ruby/issues/492))
62
+ * Use NoMethodError instead of NotImplementedError for unimplemented base class methods ([#487](https://github.com/googleapis/google-auth-library-ruby/issues/487))
63
+
64
+ ### 1.11.0 (2024-02-09)
65
+
66
+ #### Features
67
+
68
+ * Deprecate the positional argument for callback_uri, and introduce keyword argument instead ([#475](https://github.com/googleapis/google-auth-library-ruby/issues/475))
69
+
70
+ ### 1.10.0 (2024-02-08)
71
+
72
+ #### Features
73
+
74
+ * add PKCE to 3 Legged OAuth exchange ([#471](https://github.com/googleapis/google-auth-library-ruby/issues/471))
75
+ #### Bug Fixes
76
+
77
+ * Client library credentials provide correct self-signed JWT and external account behavior when loading from a file path or JSON data ([#474](https://github.com/googleapis/google-auth-library-ruby/issues/474))
78
+ * Prioritize universe domain specified in GCECredentials arguments over metadata-fetched value ([#472](https://github.com/googleapis/google-auth-library-ruby/issues/472))
79
+
80
+ ### 1.9.2 (2024-01-25)
81
+
82
+ #### Bug Fixes
83
+
84
+ * Prevent access tokens from being fetched at service account construction in the self-signed-jwt case ([#467](https://github.com/googleapis/google-auth-library-ruby/issues/467))
85
+
86
+ ### 1.9.1 (2023-12-12)
87
+
88
+ #### Bug Fixes
89
+
90
+ * update expires_in for cached metadata-retrieved tokens ([#464](https://github.com/googleapis/google-auth-library-ruby/issues/464))
91
+
92
+ ### 1.9.0 (2023-12-07)
93
+
94
+ #### Features
95
+
96
+ * Include universe_domain in credentials ([#460](https://github.com/googleapis/google-auth-library-ruby/issues/460))
97
+ * Use google-cloud-env for more robust Metadata Service access ([#459](https://github.com/googleapis/google-auth-library-ruby/issues/459))
98
+
3
99
  ### 1.8.1 (2023-09-19)
4
100
 
5
101
  #### Documentation
data/README.md CHANGED
@@ -64,6 +64,15 @@ well as a web variant tailored toward Rack-based applications.
64
64
  The authorizers are intended for authorization use cases. For sign-on,
65
65
  see [Google Identity Platform](https://developers.google.com/identity/)
66
66
 
67
+ ## Important notes
68
+
69
+ If you accept a credential configuration (credential JSON/File/Stream) from an
70
+ external source for authentication to Google Cloud, you must validate it before
71
+ providing it to any Google API or library. Providing an unvalidated credential
72
+ configuration to Google APIs can compromise the security of your systems and data.
73
+ For more information, refer to [Validate credential configurations from external
74
+ sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
75
+
67
76
  ### Example (Web)
68
77
 
69
78
  ```ruby
@@ -97,6 +106,45 @@ get('/oauth2callback') do
97
106
  end
98
107
  ```
99
108
 
109
+ ### Example (Web with PKCE)
110
+
111
+ Proof Key for Code Exchange (PKCE) is an [RFC](https://www.rfc-editor.org/rfc/rfc7636) that aims to prevent malicious operating system processes from hijacking an OAUTH 2.0 exchange. PKCE mitigates the above vulnerability by including `code_challenge` and `code_challenge_method` parameters in the Authorization Request and a `code_verifier` parameter in the Access Token Request.
112
+
113
+ ```ruby
114
+ require 'googleauth'
115
+ require 'googleauth/web_user_authorizer'
116
+ require 'googleauth/stores/redis_token_store'
117
+ require 'redis'
118
+
119
+ client_id = Google::Auth::ClientId.from_file('/path/to/client_secrets.json')
120
+ scope = ['https://www.googleapis.com/auth/drive']
121
+ token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new)
122
+ authorizer = Google::Auth::WebUserAuthorizer.new(
123
+ client_id, scope, token_store, '/oauth2callback')
124
+
125
+
126
+ get('/authorize') do
127
+ # NOTE: Assumes the user is already authenticated to the app
128
+ user_id = request.session['user_id']
129
+ # User needs to take care of generating the code_verifier and storing it in
130
+ # the session.
131
+ request.session['code_verifier'] ||= Google::Auth::WebUserAuthorizer.generate_code_verifier
132
+ authorizer.code_verifier = request.session['code_verifier']
133
+ credentials = authorizer.get_credentials(user_id, request)
134
+ if credentials.nil?
135
+ redirect authorizer.get_authorization_url(login_hint: user_id, request: request)
136
+ end
137
+ # Credentials are valid, can call APIs
138
+ # ...
139
+ end
140
+
141
+ get('/oauth2callback') do
142
+ target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(
143
+ request)
144
+ redirect target_url
145
+ end
146
+ ```
147
+
100
148
  ### Example (Command Line) [Deprecated]
101
149
 
102
150
  The Google Auth OOB flow has been discontiued on January 31, 2023. The OOB flow is a legacy flow that is no longer considered secure. To continue using Google Auth, please migrate your applications to a more secure flow. For more information on how to do this, please refer to this [OOB Migration](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration) guide.
@@ -217,7 +265,7 @@ Custom storage implementations can also be used. See
217
265
 
218
266
  ## Supported Ruby Versions
219
267
 
220
- This library is supported on Ruby 2.6+.
268
+ This library is supported on Ruby 3.0+.
221
269
 
222
270
  Google provides official support for Ruby versions that are actively supported
223
271
  by Ruby Core—that is, Ruby versions that are either in normal maintenance or
@@ -0,0 +1,155 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/credentials_loader"
17
+
18
+ module Google
19
+ module Auth
20
+ ##
21
+ # Implementation of Google API Key authentication.
22
+ #
23
+ # API Keys are text strings. They don't have an associated JSON file.
24
+ #
25
+ # The end-user is managing their API Keys directly, not via
26
+ # an authentication library.
27
+ #
28
+ # API Keys provide project information for an API request.
29
+ # API Keys don't reference an IAM principal, they do not expire,
30
+ # and cannot be refreshed.
31
+ #
32
+ class APIKeyCredentials
33
+ include Google::Auth::BaseClient
34
+
35
+ # @private Authorization header key
36
+ API_KEY_HEADER = "x-goog-api-key".freeze
37
+
38
+ # @private Environment variable containing API key
39
+ API_KEY_VAR = "GOOGLE_API_KEY".freeze
40
+
41
+ # @return [String] The API key
42
+ attr_reader :api_key
43
+
44
+ # @return [String] The universe domain of the universe
45
+ # this API key is for
46
+ attr_accessor :universe_domain
47
+
48
+ class << self
49
+ # Creates an APIKeyCredentials from the environment.
50
+ # Checks the ENV['GOOGLE_API_KEY'] variable.
51
+ #
52
+ # @param [String] _scope
53
+ # The scope to use for OAuth. Not used by API key auth.
54
+ # @param [Hash] options
55
+ # The options to pass to the credentials instance
56
+ #
57
+ # @return [Google::Auth::APIKeyCredentials, nil]
58
+ # Credentials if the API key environment variable is present,
59
+ # nil otherwise
60
+ def from_env _scope = nil, options = {}
61
+ api_key = ENV[API_KEY_VAR]
62
+ return nil if api_key.nil? || api_key.empty?
63
+ new options.merge(api_key: api_key)
64
+ end
65
+
66
+ # Create the APIKeyCredentials.
67
+ #
68
+ # @param [Hash] options The credentials options
69
+ # @option options [String] :api_key
70
+ # The API key to use for authentication
71
+ # @option options [String] :universe_domain
72
+ # The universe domain of the universe this API key
73
+ # belongs to (defaults to googleapis.com)
74
+ # @return [Google::Auth::APIKeyCredentials]
75
+ def make_creds options = {}
76
+ new options
77
+ end
78
+ end
79
+
80
+ # Initialize the APIKeyCredentials.
81
+ #
82
+ # @param [Hash] options The credentials options
83
+ # @option options [String] :api_key
84
+ # The API key to use for authentication
85
+ # @option options [String] :universe_domain
86
+ # The universe domain of the universe this API key
87
+ # belongs to (defaults to googleapis.com)
88
+ def initialize options = {}
89
+ raise ArgumentError, "API key must be provided" if options[:api_key].nil? || options[:api_key].empty?
90
+ @api_key = options[:api_key]
91
+ @universe_domain = options[:universe_domain] || "googleapis.com"
92
+ end
93
+
94
+ # Determines if the credentials object has expired.
95
+ # Since API keys don't expire, this always returns false.
96
+ #
97
+ # @param [Fixnum] _seconds
98
+ # The optional timeout in seconds since the last refresh
99
+ # @return [Boolean]
100
+ # True if the token has expired, false otherwise.
101
+ def expires_within? _seconds
102
+ false
103
+ end
104
+
105
+ # Creates a duplicate of these credentials.
106
+ #
107
+ # @param [Hash] options Additional options for configuring the credentials
108
+ # @return [Google::Auth::APIKeyCredentials]
109
+ def duplicate options = {}
110
+ self.class.new(
111
+ api_key: options[:api_key] || @api_key,
112
+ universe_domain: options[:universe_domain] || @universe_domain
113
+ )
114
+ end
115
+
116
+ # Updates the provided hash with the API Key header.
117
+ #
118
+ # The `apply!` method modifies the provided hash in place, adding the
119
+ # `x-goog-api-key` header with the API Key value.
120
+ #
121
+ # The API Key is hashed before being logged for security purposes.
122
+ #
123
+ # NB: this method typically would be called through `updater_proc`.
124
+ # Some older clients call it directly though, so it has to be public.
125
+ #
126
+ # @param [Hash] a_hash The hash to which the API Key header should be added.
127
+ # This is typically a hash representing the request headers. This hash
128
+ # will be modified in place.
129
+ # @param [Hash] _opts Additional options (currently not used). Included
130
+ # for consistency with the `BaseClient` interface.
131
+ # @return [Hash] The modified hash (the same hash passed as the `a_hash`
132
+ # argument).
133
+ def apply! a_hash, _opts = {}
134
+ a_hash[API_KEY_HEADER] = @api_key
135
+ logger&.debug do
136
+ hash = Digest::SHA256.hexdigest @api_key
137
+ Google::Logging::Message.from message: "Sending API key auth token. (sha256:#{hash})"
138
+ end
139
+ a_hash
140
+ end
141
+
142
+ protected
143
+
144
+ # The token type should be :api_key
145
+ def token_type
146
+ :api_key
147
+ end
148
+
149
+ # We don't need to fetch access tokens for API key auth
150
+ def fetch_access_token! _options = {}
151
+ nil
152
+ end
153
+ end
154
+ end
155
+ end
@@ -55,11 +55,7 @@ module Google
55
55
  DefaultCredentials.from_well_known_path(scope, options) ||
56
56
  DefaultCredentials.from_system_default_path(scope, options)
57
57
  return creds unless creds.nil?
58
- unless GCECredentials.on_gce? options
59
- # Clear cache of the result of GCECredentials.on_gce?
60
- GCECredentials.reset_cache
61
- raise NOT_FOUND_ERROR
62
- end
58
+ raise NOT_FOUND_ERROR unless GCECredentials.on_gce? options
63
59
  GCECredentials.new options.merge(scope: scope)
64
60
  end
65
61
  end
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "google/logging/message"
16
+
15
17
  module Google
16
18
  # Module Auth provides classes that provide Google-specific authorization
17
19
  # used to access Google APIs.
@@ -29,7 +31,14 @@ module Google
29
31
  # fetch the access token there is currently not one, or if the client
30
32
  # has expired
31
33
  fetch_access_token! opts if needs_access_token?
32
- a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
34
+ token = send token_type
35
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{token}"
36
+ logger&.debug do
37
+ hash = Digest::SHA256.hexdigest token
38
+ Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})"
39
+ end
40
+
41
+ a_hash[AUTH_METADATA_KEY]
33
42
  end
34
43
 
35
44
  # Returns a clone of a_hash updated with the authentication token
@@ -63,17 +72,20 @@ module Google
63
72
  end
64
73
 
65
74
  def expires_within?
66
- raise NotImplementedError
75
+ raise NoMethodError, "expires_within? not implemented"
67
76
  end
68
77
 
78
+ # The logger used to log operations on this client, such as token refresh.
79
+ attr_accessor :logger
80
+
69
81
  private
70
82
 
71
83
  def token_type
72
- raise NotImplementedError
84
+ raise NoMethodError, "token_type not implemented"
73
85
  end
74
86
 
75
87
  def fetch_access_token!
76
- raise NotImplementedError
88
+ raise NoMethodError, "fetch_access_token! not implemented"
77
89
  end
78
90
  end
79
91
  end
@@ -0,0 +1,148 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/base_client"
16
+
17
+ module Google
18
+ module Auth
19
+ ##
20
+ # Implementation of Bearer Token authentication scenario.
21
+ #
22
+ # Bearer tokens are strings representing an authorization grant.
23
+ # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything
24
+ # that is sent as a `Bearer` in an `Authorization` header.
25
+ #
26
+ # Not all 'authentication' strings can be used with this class,
27
+ # e.g. an API key cannot since API keys are sent in a
28
+ # `x-goog-api-key` header or as a query parameter.
29
+ #
30
+ # This class should be used when the end-user is managing the
31
+ # authentication token separately, e.g. with a separate service.
32
+ # This means that tasks like tracking the lifetime of and
33
+ # refreshing the token are outside the scope of this class.
34
+ #
35
+ # There is no JSON representation for this type of credentials.
36
+ # If the end-user has credentials in JSON format they should typically
37
+ # use the corresponding credentials type, e.g. ServiceAccountCredentials
38
+ # with the service account JSON.
39
+ #
40
+ class BearerTokenCredentials
41
+ include Google::Auth::BaseClient
42
+
43
+ # @private Authorization header name
44
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
45
+
46
+ # @return [String] The token to be sent as a part of Bearer claim
47
+ attr_reader :token
48
+ # The following aliasing is needed for BaseClient since it sends :token_type
49
+ alias bearer_token token
50
+
51
+ # @return [Time, nil] The token expiration time provided by the end-user.
52
+ attr_reader :expires_at
53
+
54
+ # @return [String] The universe domain of the universe
55
+ # this token is for
56
+ attr_accessor :universe_domain
57
+
58
+ class << self
59
+ # Create the BearerTokenCredentials.
60
+ #
61
+ # @param [Hash] options The credentials options
62
+ # @option options [String] :token The bearer token to use.
63
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
64
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
65
+ # If `expires_at` is `nil`, it is treated as "token never expires".
66
+ # @option options [String] :universe_domain The universe domain of the universe
67
+ # this token is for (defaults to googleapis.com)
68
+ # @return [Google::Auth::BearerTokenCredentials]
69
+ def make_creds options = {}
70
+ new options
71
+ end
72
+ end
73
+
74
+ # Initialize the BearerTokenCredentials.
75
+ #
76
+ # @param [Hash] options The credentials options
77
+ # @option options [String] :token The bearer token to use.
78
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
79
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
80
+ # If `expires_at` is `nil`, it is treated as "token never expires".
81
+ # @option options [String] :universe_domain The universe domain of the universe
82
+ # this token is for (defaults to googleapis.com)
83
+ def initialize options = {}
84
+ raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
85
+ @token = options[:token]
86
+ @expires_at = case options[:expires_at]
87
+ when Time
88
+ options[:expires_at]
89
+ when Numeric
90
+ Time.at options[:expires_at]
91
+ end
92
+
93
+ @universe_domain = options[:universe_domain] || "googleapis.com"
94
+ end
95
+
96
+ # Determines if the credentials object has expired.
97
+ #
98
+ # @param [Numeric] seconds The optional timeout in seconds.
99
+ # @return [Boolean] True if the token has expired, false otherwise, or
100
+ # if the expires_at was not provided.
101
+ def expires_within? seconds
102
+ return false if @expires_at.nil? # Treat nil expiration as "never expires"
103
+ Time.now + seconds >= @expires_at
104
+ end
105
+
106
+ # Creates a duplicate of these credentials.
107
+ #
108
+ # @param [Hash] options Additional options for configuring the credentials
109
+ # @option options [String] :token The bearer token to use.
110
+ # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time
111
+ # object or a number of seconds since epoch.
112
+ # @option options [String] :universe_domain The universe domain (defaults to googleapis.com)
113
+ # @return [Google::Auth::BearerTokenCredentials]
114
+ def duplicate options = {}
115
+ self.class.new(
116
+ token: options[:token] || @token,
117
+ expires_at: options[:expires_at] || @expires_at,
118
+ universe_domain: options[:universe_domain] || @universe_domain
119
+ )
120
+ end
121
+
122
+ protected
123
+
124
+ ##
125
+ # BearerTokenCredentials do not support fetching a new token.
126
+ #
127
+ # If the token has an expiration time and is expired, this method will
128
+ # raise an error.
129
+ #
130
+ # @param [Hash] _options Options for fetching a new token (not used).
131
+ # @return [nil] Always returns nil.
132
+ # @raise [StandardError] If the token is expired.
133
+ def fetch_access_token! _options = {}
134
+ if @expires_at && Time.now >= @expires_at
135
+ raise "Bearer token has expired."
136
+ end
137
+
138
+ nil
139
+ end
140
+
141
+ private
142
+
143
+ def token_type
144
+ :bearer_token
145
+ end
146
+ end
147
+ end
148
+ end