end_point_blank 0.6.0 → 0.12.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 +4 -4
- data/CHANGELOG.md +621 -0
- data/README.md +236 -19
- data/end_point_blank.gemspec +5 -3
- data/lib/end_point_blank/access_tokens.rb +244 -25
- data/lib/end_point_blank/authorization.rb +105 -20
- data/lib/end_point_blank/commands/authentication_cache.rb +141 -19
- data/lib/end_point_blank/commands/basic_authenticate.rb +66 -2
- data/lib/end_point_blank/commands/bearer_generate.rb +36 -0
- data/lib/end_point_blank/commands/endpoint_authorize.rb +46 -1
- data/lib/end_point_blank/commands/endpoint_update.rb +2 -2
- data/lib/end_point_blank/commands/generate_access_token.rb +241 -8
- data/lib/end_point_blank/commands/http.rb +20 -1
- data/lib/end_point_blank/configuration.rb +111 -4
- data/lib/end_point_blank/configuration_error.rb +18 -0
- data/lib/end_point_blank/rails/authenticated.rb +62 -7
- data/lib/end_point_blank/rails/authorized.rb +9 -13
- data/lib/end_point_blank/target_url.rb +57 -0
- data/lib/end_point_blank/token_unavailable_error.rb +102 -0
- data/lib/end_point_blank/unauthorized_error.rb +81 -1
- data/lib/end_point_blank/version.rb +1 -1
- data/lib/end_point_blank/writers/delayed_writer.rb +131 -21
- data/lib/end_point_blank/writers/direct_writer.rb +1 -1
- data/lib/end_point_blank/writers/exception_writer.rb +11 -2
- data/lib/end_point_blank/writers/log_writer.rb +1 -1
- data/lib/end_point_blank/writers/request_writer.rb +1 -0
- data/lib/end_point_blank/writers/response_writer.rb +1 -0
- data/lib/end_point_blank/writers/shared.rb +35 -4
- data/lib/end_point_blank.rb +248 -3
- metadata +15 -10
- data/lib/end_point_blank/loggers/logger.rb +0 -30
|
@@ -1,12 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "singleton"
|
|
4
4
|
|
|
5
5
|
module EndPointBlank
|
|
6
6
|
module Commands
|
|
7
7
|
# Thread-safe singleton cache for storing authentication credentials.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# It is scoped to a single Ruby process: there is nothing behind it but
|
|
9
|
+
# a plain Hash (no Rails.cache, Redis, or other shared store), so a
|
|
10
|
+
# Puma or Unicorn worker, and every separate app instance, each hold
|
|
11
|
+
# their own cache and their own view of cache_ttl. Nothing described
|
|
12
|
+
# below crosses a process boundary.
|
|
13
|
+
#
|
|
14
|
+
# Capped at MAX_SIZE entries. When full, stale entries are evicted first;
|
|
15
|
+
# if still at capacity, whichever remaining entry's recorded expires_at
|
|
16
|
+
# is earliest is removed (see the note on make_room_for below -- that is
|
|
17
|
+
# a heuristic, not exactly "closest to becoming stale").
|
|
18
|
+
#
|
|
19
|
+
# Validity is re-derived on every read against the cache_ttl configured
|
|
20
|
+
# AT READ TIME (sc-755), not just against the expiry computed when the
|
|
21
|
+
# entry was written:
|
|
22
|
+
#
|
|
23
|
+
# - If cache_ttl is currently 0 (disabled), the ENTIRE cache **in
|
|
24
|
+
# this process** is cleared -- every entry, not just the one looked
|
|
25
|
+
# up or being stored -- on both a read and a store that observe the
|
|
26
|
+
# disabled state. (Amended 2026-09-14: an earlier version of this
|
|
27
|
+
# cache deleted only the single key being read, which let a
|
|
28
|
+
# *different* key go on answering after cache_ttl was raised again
|
|
29
|
+
# -- a revoked grant could resurrect. This matches the Elixir SDK's
|
|
30
|
+
# sc-660 `AuthCache.clear/0`, which clears the whole ETS table on
|
|
31
|
+
# the disabled get/put path.)
|
|
32
|
+
# Because each process's cache is independent, this does not reach
|
|
33
|
+
# any other worker or instance: each one clears only when it has
|
|
34
|
+
# itself observed cache_ttl disabled and then handled an Authorized
|
|
35
|
+
# request (or a direct cache call) while disabled. A worker sitting
|
|
36
|
+
# idle, or one that has not yet picked up the new config, keeps
|
|
37
|
+
# serving whatever it already cached until it does -- nothing here
|
|
38
|
+
# coordinates that across processes, and there is no fleet-wide
|
|
39
|
+
# schedule for when, or whether, any given process's turn comes.
|
|
40
|
+
# Known residual, left unaddressed here rather than fixed: a disable
|
|
41
|
+
# followed by a re-enable with **no cache read or store in between**
|
|
42
|
+
# flushes nothing, because nothing ever observed the disabled state
|
|
43
|
+
# to trigger the clear. This story does not add configure-time
|
|
44
|
+
# flushing.
|
|
45
|
+
# - Otherwise an entry HITs only while it is within BOTH its original
|
|
46
|
+
# write-time expiry (so raising cache_ttl later never extends an
|
|
47
|
+
# entry's life) AND the CURRENT cache_ttl measured from its write
|
|
48
|
+
# time (so lowering cache_ttl shortens already-cached entries on
|
|
49
|
+
# their next read, without waiting for the original expiry).
|
|
50
|
+
#
|
|
51
|
+
# This is deliberately not "remaining time until the original expiry <=
|
|
52
|
+
# the new ttl" -- that arithmetic drifts back into looking valid once
|
|
53
|
+
# enough real time has passed, even though the entry is older than the
|
|
54
|
+
# new ttl allows. Elapsed-since-write is compared to the current ttl
|
|
55
|
+
# directly instead.
|
|
56
|
+
#
|
|
57
|
+
# cache_ttl is always a non-negative Integer by the time this cache reads
|
|
58
|
+
# it: Configuration#cache_ttl= refuses nil, negative numbers and
|
|
59
|
+
# non-Integers at configure time (sc-970), so there is no invalid value
|
|
60
|
+
# left for this class to detect at read or store time.
|
|
10
61
|
class AuthenticationCache
|
|
11
62
|
include Singleton
|
|
12
63
|
|
|
@@ -23,16 +74,17 @@ module EndPointBlank
|
|
|
23
74
|
# @return [Object] The stored credentials
|
|
24
75
|
def store(key, credentials)
|
|
25
76
|
return unless credentials
|
|
77
|
+
|
|
26
78
|
@mutex.synchronize do
|
|
79
|
+
ttl = current_ttl
|
|
80
|
+
# A store made while disabled clears whatever is already cached,
|
|
81
|
+
# same as a disabled read -- see the class comment -- and inserts
|
|
82
|
+
# nothing itself.
|
|
83
|
+
next if clear_if_disabled!(ttl)
|
|
84
|
+
|
|
27
85
|
now = Time.now
|
|
28
|
-
|
|
29
|
-
@cache
|
|
30
|
-
# Evict the entry expiring soonest if still at capacity
|
|
31
|
-
if @cache.size >= MAX_SIZE
|
|
32
|
-
oldest_key = @cache.min_by { |_, v| v[:expired_at] }.first
|
|
33
|
-
@cache.delete(oldest_key)
|
|
34
|
-
end
|
|
35
|
-
@cache[key] = { expired_at: now + ::EndPointBlank::Configuration.instance.cache_ttl, credentials: credentials }
|
|
86
|
+
make_room_for(now, ttl)
|
|
87
|
+
@cache[key] = { written_at: now, expires_at: now + ttl, credentials: credentials }
|
|
36
88
|
end
|
|
37
89
|
end
|
|
38
90
|
|
|
@@ -41,11 +93,8 @@ module EndPointBlank
|
|
|
41
93
|
# @return [Object, nil] The stored credentials or nil if not found
|
|
42
94
|
def retrieve(key)
|
|
43
95
|
@mutex.synchronize do
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
else
|
|
47
|
-
nil
|
|
48
|
-
end
|
|
96
|
+
entry = valid_entry(key)
|
|
97
|
+
entry && entry[:credentials]
|
|
49
98
|
end
|
|
50
99
|
end
|
|
51
100
|
|
|
@@ -53,9 +102,7 @@ module EndPointBlank
|
|
|
53
102
|
# @param key [String, Symbol] The identifier to check
|
|
54
103
|
# @return [Boolean] True if credentials exist, false otherwise
|
|
55
104
|
def exists?(key)
|
|
56
|
-
@mutex.synchronize
|
|
57
|
-
@cache.key?(key) && @cache[key][:expired_at] > Time.now
|
|
58
|
-
end
|
|
105
|
+
@mutex.synchronize { !valid_entry(key).nil? }
|
|
59
106
|
end
|
|
60
107
|
|
|
61
108
|
# Remove credentials from the cache
|
|
@@ -90,6 +137,81 @@ module EndPointBlank
|
|
|
90
137
|
@cache.size
|
|
91
138
|
end
|
|
92
139
|
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Looks up `key` against the CURRENT cache_ttl.
|
|
144
|
+
#
|
|
145
|
+
# If the cache is disabled, the entire cache is cleared -- see the
|
|
146
|
+
# class comment -- and this returns nil regardless of whether `key`
|
|
147
|
+
# was ever present (a disabled read of a key that was never cached
|
|
148
|
+
# still has to flush everything else).
|
|
149
|
+
#
|
|
150
|
+
# Otherwise, if `key` is present but stale under the current ttl, only
|
|
151
|
+
# that single entry is deleted. Must be called with @mutex already
|
|
152
|
+
# held: every delete here runs in the same critical section as the
|
|
153
|
+
# read that decided it was stale, so a concurrently-written fresh
|
|
154
|
+
# entry for the same key can never be the one removed.
|
|
155
|
+
def valid_entry(key)
|
|
156
|
+
ttl = current_ttl
|
|
157
|
+
return nil if clear_if_disabled!(ttl)
|
|
158
|
+
|
|
159
|
+
entry = @cache[key]
|
|
160
|
+
return nil unless entry
|
|
161
|
+
|
|
162
|
+
if fresh?(entry, Time.now, ttl)
|
|
163
|
+
entry
|
|
164
|
+
else
|
|
165
|
+
@cache.delete(key)
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Already validated by Configuration#cache_ttl= -- see the class
|
|
171
|
+
# comment.
|
|
172
|
+
def current_ttl
|
|
173
|
+
::EndPointBlank::Configuration.instance.cache_ttl
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def cache_disabled?(ttl)
|
|
177
|
+
ttl <= 0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# If `ttl` means disabled, clears the whole cache and returns true;
|
|
181
|
+
# otherwise returns false and leaves the cache untouched. Shared by
|
|
182
|
+
# the read and write paths so "disabled" clears everything identically
|
|
183
|
+
# from either one -- see the class comment.
|
|
184
|
+
def clear_if_disabled!(ttl)
|
|
185
|
+
return false unless cache_disabled?(ttl)
|
|
186
|
+
|
|
187
|
+
@cache.clear
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Evict stale entries first (using the ttl we are about to write with
|
|
192
|
+
# -- the same rule a subsequent read would apply); if still at
|
|
193
|
+
# capacity, evict whichever remaining entry's recorded expires_at is
|
|
194
|
+
# earliest. That is a heuristic, not the true "closest to becoming
|
|
195
|
+
# stale under the current ttl" (which would be
|
|
196
|
+
# min(expires_at, written_at + ttl)) -- harmless today since it only
|
|
197
|
+
# ever picks among entries `fresh?` already accepted, but not the same
|
|
198
|
+
# claim as "expires soonest".
|
|
199
|
+
def make_room_for(now, ttl)
|
|
200
|
+
@cache.delete_if { |_, v| !fresh?(v, now, ttl) }
|
|
201
|
+
return if @cache.size < MAX_SIZE
|
|
202
|
+
|
|
203
|
+
oldest_key = @cache.min_by { |_, v| v[:expires_at] }.first
|
|
204
|
+
@cache.delete(oldest_key)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# An entry is fresh only while it is within BOTH its own write-time
|
|
208
|
+
# expiry (raising ttl later never extends it) AND the currently
|
|
209
|
+
# configured ttl measured from when it was written (lowering ttl
|
|
210
|
+
# shortens it immediately). Never compare remaining-time-to-expiry
|
|
211
|
+
# against the new ttl -- see the class comment.
|
|
212
|
+
def fresh?(entry, now, ttl)
|
|
213
|
+
now < entry[:expires_at] && (now - entry[:written_at]) < ttl
|
|
214
|
+
end
|
|
93
215
|
end
|
|
94
216
|
end
|
|
95
217
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/bin/ruby
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require_relative 'http'
|
|
4
5
|
|
|
5
6
|
module EndPointBlank
|
|
@@ -12,23 +13,86 @@ module EndPointBlank
|
|
|
12
13
|
|
|
13
14
|
def authenticate(request)
|
|
14
15
|
client_auth = request.headers['Authorization']
|
|
15
|
-
|
|
16
|
+
# `Authorization.intake_header` (the no-argument `Authorization.header`
|
|
17
|
+
# before sc-1469), as EndpointAuthorize uses. It previously read
|
|
18
|
+
# `"Basic #{AuthorizationGenerate.generate}"` -- a second constant
|
|
19
|
+
# this gem has never defined, so the only caller of this command
|
|
20
|
+
# could not have reached the network even once the caller's own
|
|
21
|
+
# missing constant was fixed. Fixing one without the other just moves
|
|
22
|
+
# the NameError a frame deeper.
|
|
23
|
+
#
|
|
24
|
+
# This already returns a complete "Basic <base64>" string; wrapping
|
|
25
|
+
# it in another "Basic " would send `Basic Basic ...` and intake
|
|
26
|
+
# would refuse every request. Basic is right here: this call goes to
|
|
27
|
+
# this service's own intake, never to a provider.
|
|
28
|
+
auth = Authorization.intake_header
|
|
16
29
|
body = {
|
|
17
30
|
path: request.route_uri_pattern.to_s.gsub(/\([^)]*\)/, ''),
|
|
18
31
|
http_method: request.request_method,
|
|
19
32
|
client_auth: client_auth,
|
|
20
33
|
application: Configuration.instance.app_name,
|
|
21
34
|
endpoint_version: VersionFinder.new.find(request),
|
|
22
|
-
|
|
35
|
+
# `source_ip`, which is the key intake reads. This sent
|
|
36
|
+
# `ip_address` from the beginning -- and js, py and java copied it
|
|
37
|
+
# faithfully, so all four were wrong together until sc-320 fixed
|
|
38
|
+
# the three ports. intake ignores keys it does not cast, so nothing
|
|
39
|
+
# ever failed: `source_ip_address` was simply NULL on every
|
|
40
|
+
# authenticate row from a Rails application, and every per-source-IP
|
|
41
|
+
# question about authenticate traffic read as though there were
|
|
42
|
+
# none. EndpointAuthorize on the next path over has always sent it
|
|
43
|
+
# under the right name.
|
|
44
|
+
source_ip: request.remote_ip
|
|
23
45
|
}
|
|
24
46
|
response = Http.post(configuration.authorize_url, auth, body)
|
|
25
47
|
return nil if response.nil?
|
|
26
48
|
EndPointBlank.logger.info "Authentication response: #{response.status} - #{response.body}"
|
|
49
|
+
if response.status == 201
|
|
50
|
+
source_env_id = source_application_environment_id(response.body)
|
|
51
|
+
::EndPointBlank::Rack::EnvStore.set_source_application_environment_id(source_env_id)
|
|
52
|
+
end
|
|
27
53
|
if response.status > 299
|
|
28
54
|
EndPointBlank.logger.error "Failed to authenticate: #{response.status} - #{response.body}"
|
|
29
55
|
end
|
|
30
56
|
response
|
|
31
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def source_application_environment_id(body)
|
|
62
|
+
parsed = JSON.parse(body)
|
|
63
|
+
# `JSON.parse` succeeds on any RFC 7159 document, not just objects --
|
|
64
|
+
# `null`, `[]`, `5` and `"x"` all parse cleanly. `.dig('data', 0, ...)`
|
|
65
|
+
# on any of those raises `TypeError`/`NoMethodError`, which nothing
|
|
66
|
+
# downstream rescues: it would escape `authenticate` and turn a 201
|
|
67
|
+
# intake just granted into a 500 for the caller. Guard the shape
|
|
68
|
+
# before reading fields out of it, the same way
|
|
69
|
+
# `UnauthorizedError.reason_from` guards its own parsed body.
|
|
70
|
+
unless parsed.is_a?(Hash)
|
|
71
|
+
EndPointBlank.logger.error(
|
|
72
|
+
"Authenticated, but the authorize response body was not a JSON " \
|
|
73
|
+
"object, so this request's responses, logs and errors will not " \
|
|
74
|
+
"name their caller: body=#{body}"
|
|
75
|
+
)
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
id = parsed.dig('data', 0, 'source_application_environment_id')
|
|
80
|
+
return id if id.is_a?(String) && !id.empty?
|
|
81
|
+
|
|
82
|
+
EndPointBlank.logger.error(
|
|
83
|
+
"Authenticated, but the authorize response has no " \
|
|
84
|
+
"data[0].source_application_environment_id, so this request's responses, " \
|
|
85
|
+
"logs and errors will not name their caller: body=#{body}"
|
|
86
|
+
)
|
|
87
|
+
nil
|
|
88
|
+
rescue JSON::ParserError => error
|
|
89
|
+
EndPointBlank.logger.error(
|
|
90
|
+
"Authenticated, but the authorize response body was not valid " \
|
|
91
|
+
"JSON, so this request's responses, logs and errors will not " \
|
|
92
|
+
"name their caller: #{error.message}"
|
|
93
|
+
)
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
32
96
|
end
|
|
33
97
|
|
|
34
98
|
def self.included(base)
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module EndPointBlank
|
|
4
4
|
module Commands
|
|
5
|
+
BEARER_GENERATE_DEPRECATION =
|
|
6
|
+
"EndPointBlank::Commands::BearerGenerate is deprecated and will be removed: its header " \
|
|
7
|
+
"carries this service's own client_id/client_secret and is only valid for this service's " \
|
|
8
|
+
"own EndPointBlank intake. Never send it to a provider; use " \
|
|
9
|
+
"EndPointBlank::Authorization.header(base_url) for outbound calls (sc-1469).".freeze
|
|
10
|
+
BEARER_GENERATE_DEPRECATION_MUTEX = Mutex.new
|
|
11
|
+
private_constant :BEARER_GENERATE_DEPRECATION_MUTEX
|
|
12
|
+
|
|
5
13
|
module BearerGenerateMethods
|
|
6
14
|
module ClassMethods
|
|
7
15
|
def configuration
|
|
@@ -9,12 +17,36 @@ module EndPointBlank
|
|
|
9
17
|
end
|
|
10
18
|
|
|
11
19
|
def generate()
|
|
20
|
+
warn_deprecated
|
|
12
21
|
Base64.encode64(configuration.client_id + ":" + configuration.client_secret).gsub("\n", "")
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def auth_header
|
|
16
25
|
"Basic " + generate
|
|
17
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Once per process, not once per call: this sits on a request path,
|
|
31
|
+
# and a warning per request would bury the log it is meant to reach.
|
|
32
|
+
# The first caller wins the flag under a mutex so two threads racing
|
|
33
|
+
# the first call still print one line.
|
|
34
|
+
#
|
|
35
|
+
# Kernel#warn with category: :deprecated is printed only while
|
|
36
|
+
# Warning[:deprecated] is on (`ruby -W:deprecated`, or
|
|
37
|
+
# `Warning[:deprecated] = true`), which is how Ruby gates its own
|
|
38
|
+
# deprecations; the gem has no ActiveSupport outside Rails to lean on.
|
|
39
|
+
def warn_deprecated
|
|
40
|
+
return if @deprecation_warned
|
|
41
|
+
|
|
42
|
+
BEARER_GENERATE_DEPRECATION_MUTEX.synchronize do
|
|
43
|
+
return if @deprecation_warned
|
|
44
|
+
|
|
45
|
+
@deprecation_warned = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
warn(BEARER_GENERATE_DEPRECATION, category: :deprecated)
|
|
49
|
+
end
|
|
18
50
|
end
|
|
19
51
|
|
|
20
52
|
def self.included(base)
|
|
@@ -26,6 +58,10 @@ module EndPointBlank
|
|
|
26
58
|
# Creates a Base64-encoded string from the client_id and client_secret
|
|
27
59
|
# configured in EndPointBlank::Configuration.
|
|
28
60
|
# Use auth_header class method to get a properly formatted "Basic {credentials}" header.
|
|
61
|
+
#
|
|
62
|
+
# @deprecated The header carries this service's own client secret and is only valid
|
|
63
|
+
# for this service's own EndPointBlank intake (see Authorization.intake_header).
|
|
64
|
+
# Never send it to a provider; use Authorization.header(base_url) (sc-1469).
|
|
29
65
|
class BearerGenerate
|
|
30
66
|
include BearerGenerateMethods
|
|
31
67
|
end
|
|
@@ -40,6 +40,7 @@ module EndPointBlank
|
|
|
40
40
|
# headers would appear only on cache misses, which reads as a flaky
|
|
41
41
|
# feature rather than a missing one.
|
|
42
42
|
if (cached = cache.retrieve(cache_key))
|
|
43
|
+
record_source_application_environment_id(cached)
|
|
43
44
|
return CachedResponse.new(201, cached)
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -64,17 +65,61 @@ module EndPointBlank
|
|
|
64
65
|
# token, so the 401 retry that used to live here is gone: a 401 now
|
|
65
66
|
# means the credential is wrong, which is worth surfacing rather
|
|
66
67
|
# than retrying.
|
|
67
|
-
response = Http.post(configuration.authorize_url, Authorization.
|
|
68
|
+
response = Http.post(configuration.authorize_url, Authorization.intake_header, body)
|
|
68
69
|
|
|
69
70
|
return nil if response.nil?
|
|
70
71
|
EndPointBlank.logger.info "Authentication response: #{response.status} - #{response.body}"
|
|
71
72
|
if response.status == 201
|
|
73
|
+
record_source_application_environment_id(response.body)
|
|
72
74
|
cache.store(cache_key, response.body)
|
|
73
75
|
elsif response.status > 299
|
|
74
76
|
EndPointBlank.logger.error "Failed to authorize endpoint: #{response.status} - #{response.body}"
|
|
75
77
|
end
|
|
76
78
|
response
|
|
77
79
|
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Intake renders the grant under data[0]. The Rails controller also
|
|
84
|
+
# records this value, but the command is also used directly (today,
|
|
85
|
+
# by end_point_blank_deploy's conformance driver), so the command must
|
|
86
|
+
# carry the grant into the request store before any writer runs.
|
|
87
|
+
def record_source_application_environment_id(body)
|
|
88
|
+
parsed = JSON.parse(body)
|
|
89
|
+
# A 201 whose body is valid JSON but not a Hash -- `[]`, `null`,
|
|
90
|
+
# `"x"` -- used to reach `dig` below and raise (TypeError for an
|
|
91
|
+
# Array, NoMethodError for nil or a String), turning a request
|
|
92
|
+
# intake had actually GRANTED into a crash for this direct-caller
|
|
93
|
+
# path. The other SDKs all guard the type before digging into the
|
|
94
|
+
# body (py: isinstance on both levels, js: optional chaining,
|
|
95
|
+
# elixir: pattern match with a fallback clause); this matches them.
|
|
96
|
+
unless parsed.is_a?(Hash)
|
|
97
|
+
EndPointBlank.logger.error(
|
|
98
|
+
"Authorized, but the response body did not parse to a JSON object, so this " \
|
|
99
|
+
"request's responses, logs and errors will not name their caller: body=#{body}"
|
|
100
|
+
)
|
|
101
|
+
return ::EndPointBlank::Rack::EnvStore.set_source_application_environment_id(nil)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
id = parsed.dig('data', 0, 'source_application_environment_id')
|
|
105
|
+
# An empty string is the same absence as a missing id -- do not
|
|
106
|
+
# record it, or the log line above claiming the caller "will not be
|
|
107
|
+
# named" is contradicted by the very next line.
|
|
108
|
+
id = nil if id == ''
|
|
109
|
+
if id.nil?
|
|
110
|
+
EndPointBlank.logger.error(
|
|
111
|
+
"Authorized, but the response has no data[0].source_application_environment_id, " \
|
|
112
|
+
"so this request's responses, logs and errors will not name their caller: body=#{body}"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
::EndPointBlank::Rack::EnvStore.set_source_application_environment_id(id)
|
|
116
|
+
rescue JSON::ParserError
|
|
117
|
+
EndPointBlank.logger.error(
|
|
118
|
+
"Authorized, but the response body is not valid JSON, so this request's responses, " \
|
|
119
|
+
"logs and errors will not name their caller: body=#{body}"
|
|
120
|
+
)
|
|
121
|
+
::EndPointBlank::Rack::EnvStore.set_source_application_environment_id(nil)
|
|
122
|
+
end
|
|
78
123
|
end
|
|
79
124
|
|
|
80
125
|
def self.included(base)
|
|
@@ -19,7 +19,7 @@ module EndPointBlank
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def auth
|
|
22
|
-
Authorization.
|
|
22
|
+
Authorization.intake_header
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def write(data)
|
|
@@ -28,7 +28,7 @@ module EndPointBlank
|
|
|
28
28
|
"app_version=#{data[:app_version]}"
|
|
29
29
|
|
|
30
30
|
response = Excon.post(configuration.endpoint_update_url,
|
|
31
|
-
headers:
|
|
31
|
+
headers: EndPointBlank::Commands::Http.headers(auth),
|
|
32
32
|
body: data.to_json,
|
|
33
33
|
**EndPointBlank::Commands::Http::TIMEOUT_OPTIONS
|
|
34
34
|
)
|