noresponse 2.1.13 → 2.1.14

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: 84db2c5e4075f4ecdee9645bc083f744be747db7e1f1aa0425cee138f6c93995
4
- data.tar.gz: 467b9df984f1893478dfb6b485d59514bf99740e55858dcf8b4a84c1f3254672
3
+ metadata.gz: a9c7ea57323988c4c7426888734ef4c533166f83c713af0005ecfcb6d3a3ee43
4
+ data.tar.gz: f61a863f292e168cd2e6505479fb77346057088300d72f164b74a015647b0efc
5
5
  SHA512:
6
- metadata.gz: 68553881631884740225fe59bbb8b73e0c473b615f592ab387db4f64fac57123afd0ae03d2f3db319f1e483e8634620812f2d5e70dbc318510dccccc91a1e9c1
7
- data.tar.gz: 49accb4c21784f3075a8749b563f173b01223d61dac34c293e07746e8fefcab09cd3aaff245c7b2413359385e6c249b7a30aae93d9848e8429ee5c6364b6dd2b
6
+ metadata.gz: bb0dc8a6a3469249db874f6fc187e8697b2ce7eb0b921231e6447be07578d6484ea9b302fa03b723be8219711bd2f53889ab3d2d95233bad9826bb6d43cfac6a
7
+ data.tar.gz: 8b7bc769f6a8c3c961c5180e3707a39896af75047491a480e70a744e2fd0931aeaa44062ce705e995b9229b1e2ef1d05d0891841ea4959fc21e5cd4a56e003b9
@@ -39,17 +39,23 @@ module NoResponse
39
39
  # with a 5xx so NoResponse retries it. The message holds no response body
40
40
  # and no credentials.
41
41
  class ProviderActionError < StandardError
42
- attr_reader :provider, :status
42
+ # code is the provider's error code from the response body, such as
43
+ # Auth0's errorCode "insufficient_scope", when it sent one.
44
+ attr_reader :provider, :status, :code
43
45
 
44
- def initialize(provider, status, message)
46
+ def initialize(provider, status, message, code = nil)
45
47
  super(message)
46
48
  @provider = provider
47
49
  @status = status
50
+ @code = code
48
51
  end
49
52
  end
50
53
 
51
- # event is the verified Webhooks::Event. action is the action taken: NONE
52
- # when the policy, the event or the user id called for nothing. user_id is
54
+ # event is the verified Webhooks::Event. action is the action that ran:
55
+ # NONE when the policy, the event or the user id called for nothing. It can
56
+ # be stronger than the policy's action when the provider had to fall back:
57
+ # the auth0 provider reports BAN when it blocked the user because the
58
+ # tenant's plan cannot end sessions. user_id is
53
59
  # the provider user id acted on, set only when an action was taken. Answer
54
60
  # an endpoint.verification with Webhooks.verification_response(event).
55
61
  Result = Struct.new(:event, :action, :user_id, keyword_init: true)
@@ -102,12 +108,17 @@ module NoResponse
102
108
  return Result.new(event: event, action: NONE) if resolved.nil? || resolved.to_s.empty?
103
109
 
104
110
  resolved = resolved.to_s
105
- if action == BAN
111
+ # A provider may define act(action, user_id), returning the action that
112
+ # ran; the auth0 provider does, to report a block it had to fall back to.
113
+ ran = action
114
+ if provider.respond_to?(:act)
115
+ ran = provider.act(action, resolved)
116
+ elsif action == BAN
106
117
  provider.ban(resolved)
107
118
  else
108
119
  provider.revoke_sessions(resolved)
109
120
  end
110
- Result.new(event: event, action: action, user_id: resolved)
121
+ Result.new(event: event, action: ACTIONS.include?(ran) ? ran : action, user_id: resolved)
111
122
  end
112
123
 
113
124
  # Clerk Backend API. revoke_sessions lists the user's active sessions
@@ -146,8 +157,18 @@ module NoResponse
146
157
  # (PATCH /users/{id}), then does the same. Pass domain (such as
147
158
  # example.eu.auth0.com; the API is https://{domain}/api/v2) or base_url.
148
159
  # token is a Management API access token with update:users,
149
- # delete:sessions and delete:refresh_tokens. A 404 on any call (user
150
- # already gone) counts as done, so a repeated delivery is safe.
160
+ # delete:sessions, delete:refresh_tokens, read:device_credentials and
161
+ # delete:device_credentials. A 404 on any call (user already gone)
162
+ # counts as done, so a repeated delivery is safe.
163
+ #
164
+ # Auth0 offers the sessions and refresh-token endpoints on Enterprise
165
+ # plans only; on other plans they answer 403 feature_not_enabled. Then
166
+ # refresh tokens are deleted one by one through /device-credentials
167
+ # (types refresh_token and rotating_refresh_token), which every plan
168
+ # has. A login session cannot be ended, so revoke_sessions blocks the
169
+ # user before deleting the refresh tokens, and act reports BAN. Unblock
170
+ # the user in the dashboard to undo it. Any other refusal, such as 403
171
+ # insufficient_scope, raises.
151
172
  def auth0(token:, domain: nil, base_url: nil, http_client: nil)
152
173
  raise ArgumentError, 'auth0 needs token' if token.to_s.empty?
153
174
 
@@ -191,7 +212,9 @@ module NoResponse
191
212
  end
192
213
  status, text = perform(method, uri, headers, payload)
193
214
  return nil if allow_404 && status == 404
194
- raise ProviderActionError.new(@name, status, "#{@name} #{method} #{path} answered #{status}") unless status.between?(200, 299)
215
+ unless status.between?(200, 299)
216
+ raise ProviderActionError.new(@name, status, "#{@name} #{method} #{path} answered #{status}", error_code(text))
217
+ end
195
218
  return nil if text.nil? || text.empty?
196
219
 
197
220
  begin
@@ -203,6 +226,17 @@ module NoResponse
203
226
 
204
227
  private
205
228
 
229
+ # The error code in a provider's error body: Auth0's errorCode, or a top-level code.
230
+ def error_code(text)
231
+ body = JSON.parse(text.to_s)
232
+ return nil unless body.is_a?(Hash)
233
+
234
+ code = body['errorCode'] || body['code']
235
+ code.is_a?(String) && !code.empty? ? code : nil
236
+ rescue JSON::ParserError
237
+ nil
238
+ end
239
+
206
240
  def perform(method, uri, headers, payload)
207
241
  return @http_client.call(method, uri, headers, payload) if @http_client
208
242
 
@@ -296,6 +330,8 @@ module NoResponse
296
330
  end
297
331
 
298
332
  class Auth0
333
+ PAGE = 100
334
+
299
335
  def initialize(caller_)
300
336
  @call = caller_
301
337
  end
@@ -303,15 +339,79 @@ module NoResponse
303
339
  def name = 'auth0'
304
340
 
305
341
  def revoke_sessions(user_id)
306
- user = Segment.encode(user_id)
307
- @call.call('DELETE', "/users/#{user}/sessions", allow_404: true)
308
- @call.call('DELETE', "/users/#{user}/refresh-tokens", allow_404: true)
342
+ revoke(user_id)
309
343
  nil
310
344
  end
311
345
 
312
346
  def ban(user_id)
313
- @call.call('PATCH', "/users/#{Segment.encode(user_id)}", body: { 'blocked' => true }, allow_404: true)
314
- revoke_sessions(user_id)
347
+ user = Segment.encode(user_id)
348
+ block(user)
349
+ # The block already ends a session the plan cannot delete.
350
+ delete_sessions(user)
351
+ delete_refresh_tokens(user_id, user)
352
+ nil
353
+ end
354
+
355
+ # Bans or revokes and returns the action that ran: BAN when revoking had
356
+ # to block the user.
357
+ def act(action, user_id)
358
+ if action == BAN
359
+ ban(user_id)
360
+ return BAN
361
+ end
362
+ revoke(user_id)
363
+ end
364
+
365
+ private
366
+
367
+ def revoke(user_id)
368
+ user = Segment.encode(user_id)
369
+ ended = delete_sessions(user)
370
+ # Without the sessions API a login session survives; blocking is the only way to end it.
371
+ block(user) unless ended
372
+ delete_refresh_tokens(user_id, user)
373
+ ended ? REVOKE_SESSIONS : BAN
374
+ end
375
+
376
+ # Auth0 answers 403 feature_not_enabled when the tenant's plan does not include an endpoint.
377
+ def not_on_plan?(error)
378
+ error.status == 403 && error.code == 'feature_not_enabled'
379
+ end
380
+
381
+ def block(user)
382
+ @call.call('PATCH', "/users/#{user}", body: { 'blocked' => true }, allow_404: true)
383
+ end
384
+
385
+ # Ends every session; false when the plan has no sessions API.
386
+ def delete_sessions(user)
387
+ @call.call('DELETE', "/users/#{user}/sessions", allow_404: true)
388
+ true
389
+ rescue ProviderActionError => e
390
+ raise unless not_on_plan?(e)
391
+
392
+ false
393
+ end
394
+
395
+ def delete_refresh_tokens(user_id, user)
396
+ begin
397
+ @call.call('DELETE', "/users/#{user}/refresh-tokens", allow_404: true)
398
+ return
399
+ rescue ProviderActionError => e
400
+ raise unless not_on_plan?(e)
401
+ end
402
+ # Collect every id before deleting any, so the pages do not shift underneath.
403
+ ids = []
404
+ %w[refresh_token rotating_refresh_token].each do |type|
405
+ 10.times do |page|
406
+ body = @call.call('GET', '/device-credentials',
407
+ query: [['user_id', user_id], ['type', type], ['page', page.to_s], ['per_page', PAGE.to_s]],
408
+ allow_404: true)
409
+ listed = body.is_a?(Array) ? body.grep(Hash) : []
410
+ ids.concat(listed.map { |c| c['id'] }.select { |id| id.is_a?(String) && !id.empty? })
411
+ break if listed.length < PAGE
412
+ end
413
+ end
414
+ ids.each { |id| @call.call('DELETE', "/device-credentials/#{ERB::Util.url_encode(id)}", allow_404: true) }
315
415
  end
316
416
  end
317
417
 
@@ -95,9 +95,9 @@ module NoResponse
95
95
 
96
96
  @language = "ruby"
97
97
  @openapi_doc_version = "1"
98
- @sdk_version = "2.1.13"
98
+ @sdk_version = "2.1.14"
99
99
  @gen_version = "internal"
100
- @user_agent = "noresponse-sdk/ruby 2.1.13 internal 1 noresponse"
100
+ @user_agent = "noresponse-sdk/ruby 2.1.14 internal 1 noresponse"
101
101
  end
102
102
 
103
103
  sig { returns([String, T::Hash[Symbol, String]]) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noresponse
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.13
4
+ version: 2.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - NoResponse