end_point_blank 0.6.1 → 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 +608 -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 +239 -2
- metadata +15 -10
- data/lib/end_point_blank/loggers/logger.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97bfbad03254a86968687a6f11f10c566b6357bf1294c82521fe8216261586eb
|
|
4
|
+
data.tar.gz: 28512438acaea4072288edd364cf246f307abf64cbb2e64eb6decd66f8295b0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdb4c6de628daa762e7c17925d96b51dd4cc26379531de385b61cfcb44fbf8c87c6a87f14b3c104fe4c1f5bd5ffdc953ee16f8ff36a61c0d4a90dfe9c2701ba7
|
|
7
|
+
data.tar.gz: 9fd76a38f763fd134758d9b4c89792bd983f707821008dfc9cf4f341fd8a35f53d9455c93c965d20a9649667b076f77f0359d52bc96425966cc4c4770708250d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,613 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- **Outbound calls never fall back to `Basic` any more (sc-1469).**
|
|
8
|
+
`EndPointBlank::Authorization.header(base_url)` used to answer
|
|
9
|
+
`"Basic base64(client_id:client_secret)"` whenever it could not obtain a
|
|
10
|
+
token -- a failed mint (401, 400/422, 5xx, a response with no `base_url`),
|
|
11
|
+
an intake timeout or outage -- which sent this service's own credential to
|
|
12
|
+
the provider it was calling. A client must never do that. It now answers
|
|
13
|
+
only `"Bearer <token>"`, and raises the new
|
|
14
|
+
`EndPointBlank::TokenUnavailableError` (a subclass of `EndPointBlank::Error`)
|
|
15
|
+
when no token can be had. The error carries `base_url` (stripped, see
|
|
16
|
+
below), `failure` (the `AccessTokens::Failure` for the mint), `outcome`
|
|
17
|
+
and `status`, and its message reads `Could not mint an EndPointBlank access
|
|
18
|
+
token for <url>: <reason>. EndPointBlank never sends this service's
|
|
19
|
+
client_id/client_secret to a provider, so there is no Basic-auth fallback
|
|
20
|
+
and the call must not be made without a token.` `<reason>` is one fixed
|
|
21
|
+
text per outcome, the same in every EndPointBlank SDK (for example
|
|
22
|
+
`intake refused the token request (HTTP 422); check the URL and that a
|
|
23
|
+
grant covers the target`); intake's response body and exception messages
|
|
24
|
+
never appear in it. The body is still on `failure.reason`. A mint that
|
|
25
|
+
raises anything other than `ConfigurationError` is reported as this error
|
|
26
|
+
too, with outcome `:transport_error`, `unexpected?` true, the text
|
|
27
|
+
`the token request failed unexpectedly`, and the exception as `cause`,
|
|
28
|
+
rather than escaping `header` as whatever it was.
|
|
29
|
+
|
|
30
|
+
Callers that relied on the fallback must now rescue
|
|
31
|
+
`TokenUnavailableError` and decide for themselves: retry, degrade, or fail
|
|
32
|
+
their own request.
|
|
33
|
+
|
|
34
|
+
- **The URL's userinfo, query and fragment are removed before the token
|
|
35
|
+
request (sc-1469).** They are never sent to intake, logged, or kept on the
|
|
36
|
+
error: `Authorization.header` and the public `AccessTokens` entry points
|
|
37
|
+
(`token`, `token_result`, `exists?`, `last_failure`) strip the URL to
|
|
38
|
+
scheme, host, port and path first, and the cache, the failure record and
|
|
39
|
+
every log line use that form. `TokenUnavailableError#base_url` is the
|
|
40
|
+
stripped URL, not the one passed in. intake refuses a `base_url` carrying
|
|
41
|
+
any of them, so a URL that used to fail its mint with a 422 now mints. A
|
|
42
|
+
URL that cannot be parsed into a scheme and host is refused without a
|
|
43
|
+
request: `header` raises `ArgumentError` (without repeating the URL), and
|
|
44
|
+
`AccessTokens.token_result` answers a `:request_rejected` `Failure` with no
|
|
45
|
+
status. So is a URL whose scheme is anything but `http` or `https` (matched
|
|
46
|
+
in any case, and sent lowercased): `ftp://h:21/x` used to come out as
|
|
47
|
+
`ftp://hx`, because Ruby's `URI::FTP` keeps its path without the leading
|
|
48
|
+
slash, and no provider is reached over such a scheme anyway. So, too, is a
|
|
49
|
+
URL whose port is outside 1..65535, which intake refuses: it used to cost a
|
|
50
|
+
request and a recorded failure. The host is lowercased, as intake's
|
|
51
|
+
`BaseUrl` does, so `API.example.test` and `api.example.test` now share one
|
|
52
|
+
cached token and one failure record instead of minting separately.
|
|
53
|
+
|
|
54
|
+
- **`Commands::GenerateAccessToken.token_result` reports only a request that
|
|
55
|
+
never completed as `:transport_error` (sc-1469).** That means an Excon,
|
|
56
|
+
socket, SSL or timeout error. Anything else raised while minting -- a bug,
|
|
57
|
+
such as a `NoMethodError` -- used to be filed under `:transport_error` too,
|
|
58
|
+
which read as "intake could not be reached" and dropped the exception. It
|
|
59
|
+
now propagates, and so `GenerateAccessToken.token`, `AccessTokens.token`
|
|
60
|
+
and `AccessTokens.token_result`, which used to answer `nil` or a
|
|
61
|
+
`:transport_error` `Failure` for it, now raise it; only
|
|
62
|
+
`Authorization.header` wraps it, and reports it as above.
|
|
63
|
+
|
|
64
|
+
- **`Authorization.header` with no argument is removed.** `base_url` is now
|
|
65
|
+
required, and `header(nil)` / `header("")` raise `ArgumentError`. The
|
|
66
|
+
no-argument form returned `Basic` credentials and was only ever right for
|
|
67
|
+
the SDK's own calls to its own intake; those now use the internal
|
|
68
|
+
`Authorization.intake_header`, which behaves exactly as the old no-argument
|
|
69
|
+
`header` did. Do not use it for outbound calls to a provider.
|
|
70
|
+
|
|
71
|
+
Unchanged: authenticate/authorize, token minting, endpoint updates and the
|
|
72
|
+
log/request/response writers still authenticate to intake with `Basic`.
|
|
73
|
+
|
|
74
|
+
- **A missing `client_id` or `client_secret` raises
|
|
75
|
+
`EndPointBlank::ConfigurationError`** (a subclass of `EndPointBlank::Error`)
|
|
76
|
+
when the SDK builds its own Basic header for intake, instead of sending
|
|
77
|
+
`Basic Og==`. This includes token minting, so `Authorization.header` raises
|
|
78
|
+
it too rather than reporting a transport error.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- **`EndPointBlank::AccessTokens.token_result(base_url)`** answers the token
|
|
83
|
+
String or the `AccessTokens::Failure` recorded for that very call, inside
|
|
84
|
+
the cache's lock. `token` followed by `last_failure` could report another
|
|
85
|
+
thread's reason, or none, and `Authorization.header` now uses
|
|
86
|
+
`token_result` so `TokenUnavailableError#failure` always describes its own
|
|
87
|
+
mint. `token` and `last_failure` are unchanged.
|
|
88
|
+
- **The intake hostname can be derived from `client_id` (sc-1463), off by
|
|
89
|
+
default.** New credentials carry their organization's slug as a prefix
|
|
90
|
+
(`acima-x7k2mq.<random>`), and that organization's intake answers at
|
|
91
|
+
`https://<slug>.in.endpointblank.com`. With the new
|
|
92
|
+
`c.derive_base_url_from_client_id = true`, the SDK calls that hostname when
|
|
93
|
+
neither `base_url` nor `ENDPOINTBLANK_BASE_URL` is set. It derives only when
|
|
94
|
+
the part before the first `.` has the exact shape of an organization slug
|
|
95
|
+
and something follows the dot
|
|
96
|
+
(`EndPointBlank::Configuration.client_id_slug`, the same rule as
|
|
97
|
+
app_portal's `Credentials.client_id_slug/1`); any other `client_id`,
|
|
98
|
+
including a legacy `my.client`, calls `https://in.endpointblank.com` as
|
|
99
|
+
before. The option defaults to `false` because `*.in.endpointblank.com` has
|
|
100
|
+
no DNS or TLS in production yet; with it off, every `client_id` resolves
|
|
101
|
+
exactly as in 0.11.1. It will default to `true` in a later release, once DNS
|
|
102
|
+
and TLS are live. A value other than `true` or `false` raises
|
|
103
|
+
`ArgumentError` from the assignment itself, at configure time, and the
|
|
104
|
+
`configure` call applies nothing. The logs hostname (`log_base_url`) is
|
|
105
|
+
never derived.
|
|
106
|
+
- **Every call to intake sends `x-epb-sdk: ruby/<version>` (sc-1463)**, with
|
|
107
|
+
`EndPointBlank::VERSION` as loaded: authorize, token minting, endpoint
|
|
108
|
+
updates and the request/response/log/error writers. intake ignores it
|
|
109
|
+
today; it will record the oldest version seen per credential for the move
|
|
110
|
+
gate. **This release is not that gate's minimum Ruby version:** derivation
|
|
111
|
+
is off by default here, so a host on this version with the default config
|
|
112
|
+
keeps calling `in.endpointblank.com` after its organization moves. The
|
|
113
|
+
minimum is the release that turns `derive_base_url_from_client_id` on by
|
|
114
|
+
default.
|
|
115
|
+
|
|
116
|
+
### Unchanged
|
|
117
|
+
|
|
118
|
+
- A 503 or a 429 from intake is never cached (sc-1463 conformance, now pinned
|
|
119
|
+
by specs): the authorization cache stores only a 201, and the token cache
|
|
120
|
+
stores only a token, so the next call asks intake again. The token cache is
|
|
121
|
+
still keyed on the `base_url` the mint response returns, and a 2xx without
|
|
122
|
+
one is still a failed mint (`:server_error`, "response carried a token but
|
|
123
|
+
no base_url"). intake sends `base_url` on every successful mint.
|
|
124
|
+
|
|
125
|
+
### Deprecated
|
|
126
|
+
|
|
127
|
+
- **`EndPointBlank::Commands::BearerGenerate`** (`generate` / `auth_header`)
|
|
128
|
+
now emits a one-time runtime deprecation warning
|
|
129
|
+
(`Kernel#warn(..., category: :deprecated)`, shown when
|
|
130
|
+
`Warning[:deprecated]` is on). Its header carries this service's own client
|
|
131
|
+
secret; use `EndPointBlank::Authorization.header(base_url)` for outbound
|
|
132
|
+
calls. It will be removed in a future release.
|
|
133
|
+
|
|
134
|
+
## 0.11.1
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
|
|
138
|
+
- **`EndPointBlank.configure` is now all-or-nothing.** The sc-970 review
|
|
139
|
+
found that this SDK (along with Java) applied a `configure` block's
|
|
140
|
+
assignments to the live `Configuration` singleton as the block executed,
|
|
141
|
+
so a field set *before* a later one failed validation stayed applied even
|
|
142
|
+
though the whole call raised:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
EndPointBlank.configure do |c|
|
|
146
|
+
c.app_name = "checkout" # applied immediately
|
|
147
|
+
c.cache_ttl = -1 # raises ArgumentError here
|
|
148
|
+
end
|
|
149
|
+
# app_name was left as "checkout" -- half-updated, with no error saying so
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`configure` now yields a detached copy of the configuration, and once the
|
|
153
|
+
block returns normally, writes back only the fields whose value on that
|
|
154
|
+
copy differs from an independent deep copy taken before the block ran --
|
|
155
|
+
so a rejected call leaves the live configuration exactly as it was,
|
|
156
|
+
including a field set for the very first time (e.g. `client_id` on a
|
|
157
|
+
fresh boot, before anything has ever assigned it) and an in-place edit
|
|
158
|
+
anywhere in a String, Array or Hash field (both `masking_rules` and the
|
|
159
|
+
rule Hashes in it, and a String field such as `app_name` edited with
|
|
160
|
+
`<<`, are deep-copied into the block's copy). This holds for *any*
|
|
161
|
+
exception the block raises, not only `StandardError`. Committing only
|
|
162
|
+
what the block changed, rather than every field, also means a write to a
|
|
163
|
+
field the block never touched -- made from inside the block itself (e.g.
|
|
164
|
+
`EndPointBlank.logger =`) or concurrently from another thread outside
|
|
165
|
+
`configure` -- is no longer silently reset back to what it was when the
|
|
166
|
+
copy was made.
|
|
167
|
+
|
|
168
|
+
`configure` calls are also now serialized with a Mutex held across the
|
|
169
|
+
block and the commit, so two calls can no longer overlap. Without that,
|
|
170
|
+
two calls that both succeed could still overlap -- the second starting
|
|
171
|
+
while the first is still running -- and both would build their copy from
|
|
172
|
+
the same starting snapshot. Whichever finished last would still decide
|
|
173
|
+
what to write by comparing its own copy against that same now-stale
|
|
174
|
+
snapshot rather than against whatever was live on `Configuration` by the
|
|
175
|
+
time it actually committed, so it would silently write its own change
|
|
176
|
+
over whatever the other call had already committed. Because Ruby's
|
|
177
|
+
`Mutex` is not reentrant, calling `configure` again from inside a
|
|
178
|
+
`configure` block, on the same thread, now raises `EndPointBlank::Error`
|
|
179
|
+
instead of running -- previously, with no atomicity in place at all,
|
|
180
|
+
nesting worked by accident, since both calls just mutated the live
|
|
181
|
+
singleton directly. A block that starts a different thread, has it call
|
|
182
|
+
`configure`, and then joins it will hang instead of raising, since that
|
|
183
|
+
thread is genuinely waiting on a lock this thread holds.
|
|
184
|
+
|
|
185
|
+
The copy `configure` yields (`c`, by convention) is only valid for the
|
|
186
|
+
duration of the block: once `configure` returns, whether the block
|
|
187
|
+
returned normally or raised, `c` is frozen, and every String, Array or
|
|
188
|
+
Hash value it holds is first replaced with its own frozen deep copy. A
|
|
189
|
+
write made through a reference to `c` retained past the block now always
|
|
190
|
+
raises `FrozenError` -- a reassignment (`saved.app_name = "x"`) because
|
|
191
|
+
`c` itself is frozen, and an in-place edit (`saved.masking_rules << rule`,
|
|
192
|
+
`saved.app_name << "x"`, editing a rule Hash in place) because the value
|
|
193
|
+
it points to is frozen too, not just `c`. A read that bypasses `c` --
|
|
194
|
+
`Configuration.instance.app_name`, or `EndPointBlank.logger` right after
|
|
195
|
+
`c.logger = ...` earlier in the same block -- still sees the value from
|
|
196
|
+
before the call started, not what the block has set on `c` so far, until
|
|
197
|
+
the block returns and the commit runs.
|
|
198
|
+
|
|
199
|
+
Assigning a String, Array or Hash through `c` now copies it, rather than
|
|
200
|
+
storing the object itself: `rules = [...]; EndPointBlank.configure { |c|
|
|
201
|
+
c.masking_rules = rules }; rules << extra` no longer affects the live
|
|
202
|
+
configuration, whereas on 0.11.0 it did, because `c` was the live
|
|
203
|
+
singleton and `masking_rules` held that very same array. Call `configure`
|
|
204
|
+
again to apply a further change. Objects the caller hands in by
|
|
205
|
+
reference that are not String/Array/Hash -- `logger`, `mask_hook`,
|
|
206
|
+
`version_finder` -- are unaffected by any of this: they are held by
|
|
207
|
+
reference, not copied, both during the block and after it returns, so
|
|
208
|
+
mutating one through a retained `c` (`saved.logger.level = ...`) still
|
|
209
|
+
reaches the live value, the same as mutating it through
|
|
210
|
+
`EndPointBlank.logger` or `Configuration.instance.logger` directly would.
|
|
211
|
+
|
|
212
|
+
This is generic over every `Configuration` instance variable, not a
|
|
213
|
+
hand-maintained field list, so a future validated field (e.g. sc-1265's
|
|
214
|
+
planned `cache_ttl` upper bound) is atomic under `configure`
|
|
215
|
+
automatically, with no changes needed here. Objects the caller hands in
|
|
216
|
+
by reference -- `logger`, `mask_hook`, `version_finder` -- are copied by
|
|
217
|
+
reference like any other field, not deep-copied, since they are not
|
|
218
|
+
String/Array/Hash; `configure` cannot roll back mutation the caller
|
|
219
|
+
performs on those objects themselves, and calling `configure` from more
|
|
220
|
+
than one thread only serializes calls to `configure` itself, not reads of
|
|
221
|
+
`Configuration` elsewhere.
|
|
222
|
+
|
|
223
|
+
This is sc-1266. Of the other four EndPointBlank SDKs, only Java (`java#39`)
|
|
224
|
+
had this bug and needed a code fix; JS, Python and Elixir were already
|
|
225
|
+
atomic and got test-only PRs pinning that behavior.
|
|
226
|
+
|
|
227
|
+
## 0.11.0
|
|
228
|
+
|
|
229
|
+
### Breaking
|
|
230
|
+
|
|
231
|
+
- **`cache_ttl` is validated when you set it, not when the cache first uses
|
|
232
|
+
it.** `c.cache_ttl =` now accepts only a non-negative `Integer` number of
|
|
233
|
+
seconds. Anything else raises `ArgumentError`, naming `cache_ttl` and the
|
|
234
|
+
value, from the assignment itself -- inside `EndPointBlank.configure`, so a
|
|
235
|
+
bad value stops the app at boot -- and leaves the previous value in place.
|
|
236
|
+
This is the rule sc-970 sets for every EndPointBlank SDK:
|
|
237
|
+
|
|
238
|
+
| `cache_ttl` | 0.10.0 | Now |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| never set | 300 seconds | 300 seconds (unchanged) |
|
|
241
|
+
| `0` | accepted: cache disabled | accepted: cache disabled (unchanged) |
|
|
242
|
+
| `nil` | accepted; `TypeError: can't convert NilClass into an exact number` on the first cache store | `ArgumentError` at configure time |
|
|
243
|
+
| negative, e.g. `-5` | accepted, silently behaving like `0` | `ArgumentError` at configure time |
|
|
244
|
+
| a `String`, e.g. `"abc"` | accepted; `TypeError: can't convert String into an exact number` on the first cache store | `ArgumentError` at configure time |
|
|
245
|
+
| a non-`Integer` number, e.g. `3.5` | accepted, silently used as a 3.5-second TTL | `ArgumentError` at configure time |
|
|
246
|
+
|
|
247
|
+
**If you disabled the cache with a negative value such as `-1`, change it
|
|
248
|
+
to `0`.** To get the default, omit the setting: no value assigned to
|
|
249
|
+
`cache_ttl` means "use the default".
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
|
|
253
|
+
- **Runtime `cache_ttl` changes now apply to already-cached entries.**
|
|
254
|
+
`AuthenticationCache` used to fix an entry's expiry once, at the moment it
|
|
255
|
+
was stored; changing `cache_ttl` afterwards (typically to lower it during
|
|
256
|
+
an incident, hoping to make a revocation take effect sooner) did nothing to
|
|
257
|
+
entries already in the cache -- they kept answering until their *original*
|
|
258
|
+
expiry, on the *old* TTL. Setting `cache_ttl` to a disabling value (`<= 0`)
|
|
259
|
+
did nothing at all: it neither stopped new reads from being served nor
|
|
260
|
+
removed what was already cached.
|
|
261
|
+
|
|
262
|
+
Each entry now records its write time as well as its expiry, and every
|
|
263
|
+
read re-derives validity against the `cache_ttl` configured *at read time*,
|
|
264
|
+
anchored to that write time: an entry HITs only while it is within both its
|
|
265
|
+
original expiry (so raising `cache_ttl` never extends an entry already
|
|
266
|
+
cached) and the current `cache_ttl` measured from when it was written (so
|
|
267
|
+
lowering `cache_ttl` shortens an already-cached entry's life immediately,
|
|
268
|
+
on its next read).
|
|
269
|
+
|
|
270
|
+
A disabled `cache_ttl` now MISSes, and the moment a read *or a store*
|
|
271
|
+
observes the cache disabled, the **entire cache is cleared** -- not just
|
|
272
|
+
the entry being looked up or written. An earlier version of this fix
|
|
273
|
+
deleted only the single key involved, which let a *different*,
|
|
274
|
+
already-revoked key go on answering after `cache_ttl` was raised again.
|
|
275
|
+
This matches the Elixir SDK's existing `AuthCache.clear/0` behavior on its
|
|
276
|
+
disabled get/put path. A store made while the cache is disabled still
|
|
277
|
+
inserts nothing itself, on top of clearing what was already there.
|
|
278
|
+
|
|
279
|
+
**This cache is per Ruby process** -- a plain in-memory Hash, nothing
|
|
280
|
+
shared like `Rails.cache` or Redis -- so each Puma or Unicorn worker, and
|
|
281
|
+
every separate app instance, holds its own cache and its own view of
|
|
282
|
+
`cache_ttl`. "The entire cache is cleared" means *that process's* cache
|
|
283
|
+
only: disabling `cache_ttl` does not by itself reach any other worker or
|
|
284
|
+
instance, and each one clears its own cache only once it has itself
|
|
285
|
+
observed `cache_ttl` disabled and then handled an `Authorized` request
|
|
286
|
+
(or a direct cache call) while disabled. A worker sitting idle, or one
|
|
287
|
+
that hasn't yet picked up the new config, keeps answering from whatever
|
|
288
|
+
it already cached until it does. Known residual, left unaddressed here
|
|
289
|
+
rather than fixed: a disable followed by a re-enable with **no cache read
|
|
290
|
+
or store in between** flushes nothing, because nothing ever observed the
|
|
291
|
+
disabled state to trigger the clear; this release does not add
|
|
292
|
+
configure-time flushing.
|
|
293
|
+
|
|
294
|
+
A `nil` `cache_ttl` is never read as "disabled": it is refused when it is
|
|
295
|
+
assigned -- see Breaking, above.
|
|
296
|
+
|
|
297
|
+
## 0.10.0
|
|
298
|
+
|
|
299
|
+
### Fixed
|
|
300
|
+
|
|
301
|
+
- **The send queue no longer drops duplicate payloads.** The drain took a batch
|
|
302
|
+
off the front of the pending list and then removed it with `payloads -= list`.
|
|
303
|
+
`Array#-` removes every element *equal to* one in the batch, not the ones
|
|
304
|
+
actually sent: seven byte-identical payloads with a batch size of six meant
|
|
305
|
+
six delivered and all seven gone. Nothing counted the loss, nothing logged it,
|
|
306
|
+
and the queue drained normally, so it read as if nothing had happened.
|
|
307
|
+
|
|
308
|
+
Equal payloads are ordinary rather than exotic. Payloads are hashes, and two
|
|
309
|
+
requests to the same endpoint, from the same application, in the same
|
|
310
|
+
environment differ only in high-cardinality fields; `sent_at` at millisecond
|
|
311
|
+
precision is not a reliable discriminator at ingest volumes. Any genuinely
|
|
312
|
+
duplicate pair lost a member every time it landed in an over-full batch.
|
|
313
|
+
|
|
314
|
+
Batches are now cut by position — `each_slice(BATCH_SIZE)` over the pending
|
|
315
|
+
list, with no removal step at all — which makes equality irrelevant rather
|
|
316
|
+
than merely handled. The batch size (6) is a named constant instead of a
|
|
317
|
+
`[0..5]` in the middle of the loop; the batching behaviour is otherwise
|
|
318
|
+
unchanged.
|
|
319
|
+
|
|
320
|
+
- **The authenticate payload names the caller's IP `source_ip`.** It sent
|
|
321
|
+
`ip_address`; intake reads `source_ip` and casts it into `source_ip_address`.
|
|
322
|
+
intake ignores keys it does not cast, so nothing crashed and nothing was
|
|
323
|
+
refused — `source_ip_address` was simply NULL on every authenticate row from a
|
|
324
|
+
Rails application, and any per-source-IP question about authenticate traffic
|
|
325
|
+
read as though there were no traffic.
|
|
326
|
+
|
|
327
|
+
This was not a porting slip. This gem has always sent `ip_address`, and the
|
|
328
|
+
JS, Python and Java ports copied it faithfully, so all four were wrong
|
|
329
|
+
together until those three were fixed. `EndpointAuthorize`, on the next path
|
|
330
|
+
over, has always sent `source_ip` correctly.
|
|
331
|
+
|
|
332
|
+
Because intake never read the old key, this can only begin populating a column
|
|
333
|
+
that has never held anything: no migration, and nothing to be compatible with.
|
|
334
|
+
|
|
335
|
+
## 0.9.0
|
|
336
|
+
|
|
337
|
+
### Fixed
|
|
338
|
+
|
|
339
|
+
- **One unreachable intake no longer ends telemetry for the life of the
|
|
340
|
+
process.** `Commands::Http.post` returns `nil` once its three attempts are
|
|
341
|
+
exhausted; `DirectWriter#write` passed that straight back; and the delayed
|
|
342
|
+
writer's worker loop called `response.status` on it. The resulting
|
|
343
|
+
`NoMethodError` escaped `loop do`, which is the whole body of the worker
|
|
344
|
+
thread, so the thread died — and a dead worker is not a failed delivery, it is
|
|
345
|
+
the end of delivery. Every payload enqueued afterwards sat in the queue until
|
|
346
|
+
it was pushed out by the drop-oldest bound, and nothing said so, because a
|
|
347
|
+
dead thread raises nothing and logs nothing. The trigger was the most ordinary
|
|
348
|
+
event there is: the intake briefly unreachable.
|
|
349
|
+
|
|
350
|
+
Every other telemetry drop in this gem costs one batch. This one cost all of
|
|
351
|
+
them, from one transient failure, with no recovery short of restarting the
|
|
352
|
+
host application.
|
|
353
|
+
|
|
354
|
+
- **A `nil` response is treated as the absence of an answer, not as a status.**
|
|
355
|
+
There is nothing to compare `nil` against, so it is not compared. The batch is
|
|
356
|
+
reported through `on_failure` — with `nil`, meaning *nothing answered* — and
|
|
357
|
+
named in an `error` log line that says how many payloads went with it. A
|
|
358
|
+
writer that implements neither callback is unaffected, as before.
|
|
359
|
+
|
|
360
|
+
### Changed
|
|
361
|
+
|
|
362
|
+
- **The worker loop now survives anything a delivery can throw.** The `nil`
|
|
363
|
+
above is one defect of a shape that has now been fixed in a writer three
|
|
364
|
+
times, so this release fixes the shape rather than the instance: every
|
|
365
|
+
`StandardError` raised anywhere in an iteration is caught, logged, and
|
|
366
|
+
followed by another iteration. `DirectWriter#write` calls
|
|
367
|
+
`Authorization.header` before it calls the transport, which is a second
|
|
368
|
+
unguarded path into the same loop; it is covered too, without having had to be
|
|
369
|
+
enumerated.
|
|
370
|
+
|
|
371
|
+
Recovering silently would only trade one silent failure for another, so it is
|
|
372
|
+
not silent:
|
|
373
|
+
|
|
374
|
+
- every recovered error is logged at `error` level through
|
|
375
|
+
`EndPointBlank.logger`, with the exception class, its message, the top
|
|
376
|
+
backtrace frame, and a **count of consecutive failures** — so a persistent
|
|
377
|
+
fault reads as `consecutive failure 47`, not as forty-seven
|
|
378
|
+
indistinguishable lines;
|
|
379
|
+
- consecutive failures back off, `WORKER_BACKOFF_SECONDS` (0.1s) doubling to
|
|
380
|
+
`MAX_WORKER_BACKOFF_SECONDS` (30s), so a permanently broken send path is a
|
|
381
|
+
slow loud retry rather than a hot loop with a fan attached. The counter
|
|
382
|
+
resets on the first clean pass.
|
|
383
|
+
|
|
384
|
+
What is deliberately *not* caught is anything outside `StandardError` —
|
|
385
|
+
`SystemExit`, `Interrupt`, `SignalException`, `NoMemoryError`. Those mean the
|
|
386
|
+
process itself is going down or is already broken, and a fire-and-forget
|
|
387
|
+
telemetry worker has no business arguing with that.
|
|
388
|
+
|
|
389
|
+
- **`worker_count` is documented as what it is.** The README described it as
|
|
390
|
+
"currently unused by the delayed writer (which always spins up 2 threads);
|
|
391
|
+
reserved". It has been honoured for some time: the pool is `worker_count`
|
|
392
|
+
threads, and the hardcoded 2 survives only as the fallback used when it is set
|
|
393
|
+
to `nil`.
|
|
394
|
+
|
|
395
|
+
### Unchanged
|
|
396
|
+
|
|
397
|
+
- No public API is removed or renamed. `on_success`/`on_failure` remain
|
|
398
|
+
optional, and the only change to their contract is that `on_failure` can now
|
|
399
|
+
receive `nil`, in the case where it previously could not be reached at all.
|
|
400
|
+
|
|
401
|
+
## 0.8.0
|
|
402
|
+
|
|
403
|
+
### Fixed
|
|
404
|
+
|
|
405
|
+
- **`EndPointBlank::Rails::Authenticated` works.** It could not have worked
|
|
406
|
+
once: it called `EndPointBlank::Commands::EndpointAuthenticate`, a constant
|
|
407
|
+
this gem has never defined, so every action of every controller including the
|
|
408
|
+
concern raised `NameError` from its `before_action` — before authentication
|
|
409
|
+
could succeed or fail. It now calls
|
|
410
|
+
`EndPointBlank::Commands::BasicAuthenticate`, the command that was in the tree
|
|
411
|
+
the whole time and that the JS, Java and Python SDKs each document their own
|
|
412
|
+
authenticate command as a port of.
|
|
413
|
+
|
|
414
|
+
It was implemented rather than removed because three other SDKs expose an
|
|
415
|
+
authenticate path and all three name this gem as the original — Elixir has
|
|
416
|
+
none, so sc-306's "four other SDKs" is three, but it is three of three. A
|
|
417
|
+
missing one in Ruby is a gap, not a decision. No public constant is removed:
|
|
418
|
+
`Commands::EndpointAuthenticate` never resolved, so nothing could have been
|
|
419
|
+
depending on it, and no `Commands::EndpointAuthenticate` is being introduced
|
|
420
|
+
either — that would have been a second command for a job this gem already had
|
|
421
|
+
a command for, with `BasicAuthenticate` left dead beside it.
|
|
422
|
+
|
|
423
|
+
- **`Commands::BasicAuthenticate` works.** It built its `Authorization` header
|
|
424
|
+
from `AuthorizationGenerate.generate` — a second constant this gem has never
|
|
425
|
+
defined — so repairing only the concern's constant would have moved the same
|
|
426
|
+
`NameError` one frame deeper. It now uses `Authorization.header`, as
|
|
427
|
+
`EndpointAuthorize` and all three ports of this command do. Neither defect was
|
|
428
|
+
reachable by any test or any application, which is how both survived a
|
|
429
|
+
coverage pass: a file no spec requires and no application includes is
|
|
430
|
+
invisible to a coverage number.
|
|
431
|
+
|
|
432
|
+
- **A nil answer from intake reaches the branch written for it.**
|
|
433
|
+
`authenticate!` parsed the response body on the line *above* its own
|
|
434
|
+
`if !result` guard, so a nil result died with `NoMethodError` on `nil.body`
|
|
435
|
+
and the nil branch could never run. Fixed in the same pass rather than left to
|
|
436
|
+
be uncovered by fixing the constant.
|
|
437
|
+
|
|
438
|
+
- **A refusal from `Authenticated` now says which refusal it was.** The concern
|
|
439
|
+
used `raise UnauthorizedError, "message"` — the two-argument
|
|
440
|
+
`raise Class, message` form, which calls `Class.exception(message)` and can
|
|
441
|
+
pass nothing else, so it structurally could not carry intake's status however
|
|
442
|
+
willing `UnauthorizedError` was to accept one. Every refusal would therefore
|
|
443
|
+
have arrived as the class's 401 default, including the 403 that means
|
|
444
|
+
`access_denied`. `Authorized` has always passed intake's status through, so
|
|
445
|
+
the same denial gave a caller two different answers depending on which concern
|
|
446
|
+
the controller included.
|
|
447
|
+
|
|
448
|
+
401 and 403 send an integrator to two different places: *re-check the
|
|
449
|
+
credential* versus *ask for a grant covering this endpoint*. Collapsing them
|
|
450
|
+
sends half of them to debug the wrong thing.
|
|
451
|
+
|
|
452
|
+
| intake answered | `error.status` | what it tells the integrator |
|
|
453
|
+
| --- | --- | --- |
|
|
454
|
+
| 401 | `401` | the credential was not accepted — re-check or re-issue it |
|
|
455
|
+
| 403 | `403` | the credential is fine; no grant covers this endpoint |
|
|
456
|
+
| any other non-201 | that status | intake's own verdict, verbatim |
|
|
457
|
+
| nothing at all | `503` | the check could not be made; nothing judged this caller |
|
|
458
|
+
|
|
459
|
+
The README's own suggested handler — `status: e.status` — was written as
|
|
460
|
+
though this already worked, and on the `Authorized` path it did. It now works
|
|
461
|
+
on both.
|
|
462
|
+
|
|
463
|
+
### Changed
|
|
464
|
+
|
|
465
|
+
- **An intake 5xx now reaches a caller of an `Authenticated` controller as that
|
|
466
|
+
5xx**, where it would previously have reached them as a 401. An outage in
|
|
467
|
+
intake presents as an outage rather than as a rejected credential, so a client
|
|
468
|
+
branching on `401` to trigger a re-login will no longer do so for a fault that
|
|
469
|
+
has nothing to do with its credential. The same is true of an unreachable
|
|
470
|
+
intake, which is now a 503 — this is what `Authorized` has always answered for
|
|
471
|
+
that case, and what all four other SDKs answer.
|
|
472
|
+
|
|
473
|
+
In practice no deployment can have observed the old behaviour, because the
|
|
474
|
+
concern raised `NameError` before reaching any of it. It is called out anyway
|
|
475
|
+
because the other three SDKs called out exactly this change for exactly this
|
|
476
|
+
reason, and because anyone reading their changelogs should find the Ruby entry
|
|
477
|
+
saying the same thing.
|
|
478
|
+
|
|
479
|
+
- The two concerns' refusal handling — two transcriptions of one decision, which
|
|
480
|
+
had drifted — is now one method,
|
|
481
|
+
`EndPointBlank::UnauthorizedError.refusal_from(result, action)`. Two copies is
|
|
482
|
+
how one path acquires a fix the other does not, which is precisely what
|
|
483
|
+
happened here. `Authorized` behaves exactly as before: its refusal path moved
|
|
484
|
+
into the shared method without changing the message or the status it produces
|
|
485
|
+
for any input, including the unreachable case, whose message has never carried
|
|
486
|
+
a `"Authorization failed:"` prefix and still does not.
|
|
487
|
+
- `Rails::Authorized#authorize_error_message`, a private method, is gone; its
|
|
488
|
+
body is now the shared `refusal_from`.
|
|
489
|
+
|
|
490
|
+
### Unchanged
|
|
491
|
+
|
|
492
|
+
- `UnauthorizedError.new(message)` still means what it meant: the status
|
|
493
|
+
defaults to 401, and the status remains the optional second argument. The
|
|
494
|
+
class itself is otherwise untouched — it always accepted a status, which is
|
|
495
|
+
why nothing ever complained that `authenticate!` was not passing one.
|
|
496
|
+
- `Authenticated` deliberately does **not** cache intake's answer, matching every
|
|
497
|
+
other SDK's authenticate command. `Authorized` caches as before.
|
|
498
|
+
|
|
499
|
+
## 0.7.0
|
|
500
|
+
|
|
501
|
+
### Added
|
|
502
|
+
|
|
503
|
+
- **You can now tell a rejected credential from a server that is merely
|
|
504
|
+
having a bad day.** intake answers `401` when the API credential itself is
|
|
505
|
+
refused and something else for everything else, but the SDK threw the status
|
|
506
|
+
away: `Commands::GenerateAccessToken.token` returned the parsed body no
|
|
507
|
+
matter what came back, and `AccessTokens#token` turned every failure into
|
|
508
|
+
the same `nil` and the same `"Failed to generate access token"` log line. A
|
|
509
|
+
revoked credential and a transient 503 were indistinguishable, so nothing
|
|
510
|
+
could decide whether to retry or to stop and ask a human.
|
|
511
|
+
|
|
512
|
+
Two additive entry points:
|
|
513
|
+
|
|
514
|
+
- `Commands::GenerateAccessToken.token_result(base_url)` returns an
|
|
515
|
+
immutable `Commands::AccessTokenResult` carrying `outcome`, `status` and
|
|
516
|
+
`payload`, with `#success?`, `#credential_rejected?`, `#request_rejected?`,
|
|
517
|
+
`#server_error?`, `#transport_error?` and `#failure?`.
|
|
518
|
+
- `EndPointBlank::AccessTokens.last_failure(base_url)` (and the instance
|
|
519
|
+
method) returns an `AccessTokens::Failure` — `base_url`, `outcome`,
|
|
520
|
+
`status`, `reason`, `at`, and the same predicates — describing why the
|
|
521
|
+
last mint for that URL failed, or `nil` when the last one succeeded.
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
result = EndPointBlank::Commands::GenerateAccessToken.token_result("https://api.example.com/orders")
|
|
525
|
+
result.credential_rejected? # => true; re-issue the credential, retrying will not help
|
|
526
|
+
result.status # => 401
|
|
527
|
+
|
|
528
|
+
EndPointBlank::AccessTokens.token("https://api.example.com/orders") # => nil, as before
|
|
529
|
+
EndPointBlank::AccessTokens.last_failure("https://api.example.com/orders").status # => 401
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The five outcomes are `:success` (a token really was minted: a 2xx whose
|
|
533
|
+
body parsed and carries a non-empty `token` and the non-empty `base_url` to
|
|
534
|
+
cache it under), `:credential_rejected` (401), `:request_rejected` (any
|
|
535
|
+
other 4xx), `:server_error` (5xx, any other unexpected non-2xx, and a 2xx
|
|
536
|
+
that carried nothing usable) and `:transport_error` (no usable HTTP status
|
|
537
|
+
was obtained at all).
|
|
538
|
+
|
|
539
|
+
`#success?` is worth reading precisely: it is true only when there is a
|
|
540
|
+
token on the payload to read, so a caller that branches on it never has to
|
|
541
|
+
check for one again. A success predicate that can be true while the token is
|
|
542
|
+
absent makes every caller re-check the payload by hand, and that is the
|
|
543
|
+
check that gets forgotten.
|
|
544
|
+
|
|
545
|
+
There is deliberately **no** `#retriable?` or equivalent retry/no-retry
|
|
546
|
+
boolean. intake answers `400` for an invalid `token_ttl` or a missing
|
|
547
|
+
`base_url` and `422` for an unresolvable target/source application; those
|
|
548
|
+
are as permanent as a 401, with a different remedy, and folding five honest
|
|
549
|
+
names back into one boolean is a smaller version of the bug this release
|
|
550
|
+
fixes. Branch on the outcome you can see.
|
|
551
|
+
|
|
552
|
+
- **Classification is on the HTTP status first, the body second.** A `401`
|
|
553
|
+
whose body will not parse is still `:credential_rejected` — the SDK reaches
|
|
554
|
+
intake through a proxy, and a WAF or load balancer can answer 401 with an
|
|
555
|
+
HTML page intake never generated. `:transport_error` means one thing only:
|
|
556
|
+
no usable HTTP status was obtained.
|
|
557
|
+
|
|
558
|
+
- A rejected credential now gets its own loud log line naming the remedy,
|
|
559
|
+
instead of scrolling past as the same generic failure as a network blip.
|
|
560
|
+
|
|
561
|
+
### Changed
|
|
562
|
+
|
|
563
|
+
- A response that carries a token and a `base_url` is cached only when it
|
|
564
|
+
arrived with a 2xx status. Previously any status would do, so a 4xx that
|
|
565
|
+
echoed a token back would have been cached. A 2xx that carried no token, or
|
|
566
|
+
a token with no `base_url` to key it under, is now reported as a
|
|
567
|
+
`:server_error` with its real 2xx status attached; the specific reason
|
|
568
|
+
("no token in response", "response carried a token but no base_url") still
|
|
569
|
+
appears in the log line, exactly as before.
|
|
570
|
+
- An **empty** `token` or `base_url` counts as a missing one. `""` is truthy
|
|
571
|
+
in Ruby, so a plain presence check called such a response a success and
|
|
572
|
+
handed the caller an empty bearer token to send, or cached a token under a
|
|
573
|
+
key no lookup could ever match. Both are now `:server_error` with the real
|
|
574
|
+
2xx status, and the value must be a non-empty String — an array or an object
|
|
575
|
+
where a token belongs is a broken server too.
|
|
576
|
+
- The parsed body is still attached to the result when an unusable 2xx is
|
|
577
|
+
classified `:server_error`, so `Commands::GenerateAccessToken.token` — the
|
|
578
|
+
published payload-or-`nil` accessor — hands back exactly the body it always
|
|
579
|
+
did for such a response.
|
|
580
|
+
- A response body that will not parse is logged as an error rather than
|
|
581
|
+
silently becoming `nil`. It is still classified by the status that carried
|
|
582
|
+
it, never as a transport error: unreadable under a 2xx is a `:server_error`,
|
|
583
|
+
unreadable under a 401 is still `:credential_rejected`.
|
|
584
|
+
- `AccessTokens#clear` also drops the recorded failures.
|
|
585
|
+
|
|
586
|
+
### Changed
|
|
587
|
+
|
|
588
|
+
- **`Commands::GenerateAccessToken.token` now returns `nil` unless a token was
|
|
589
|
+
actually minted.** It previously returned the symbol-keyed body of any status
|
|
590
|
+
it could read — an `error` document from a 401 or 422, or a 2xx that parsed
|
|
591
|
+
into something with no usable token in it. Each of those handed the caller a
|
|
592
|
+
truthy value for a request that produced no token, which is the failure
|
|
593
|
+
`token_result` was added to remove, one layer down.
|
|
594
|
+
|
|
595
|
+
This aligns all five SDKs with Elixir, whose equivalent has always answered
|
|
596
|
+
nil for anything that was not a mint.
|
|
597
|
+
|
|
598
|
+
**Upgrade note:** nothing in this gem calls `token` — `AccessTokens` reads
|
|
599
|
+
`token_result(base_url).payload` — so no log line or diagnostic changes. A
|
|
600
|
+
caller that read an error out of the return value should call `token_result`
|
|
601
|
+
instead: `.payload` is exactly what `token` used to hand back, now alongside
|
|
602
|
+
the outcome that explains it. A caller that only ever read `[:token]` needs
|
|
603
|
+
no change, because a body without a usable token was never something it
|
|
604
|
+
could act on.
|
|
605
|
+
|
|
606
|
+
### Compatibility
|
|
607
|
+
|
|
608
|
+
- `AccessTokens#token` still returns a token String or `nil`, and `#exists?`
|
|
609
|
+
still returns a Boolean.
|
|
610
|
+
|
|
3
611
|
## 0.6.1
|
|
4
612
|
|
|
5
613
|
### Fixed
|