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
data/README.md
CHANGED
|
@@ -8,8 +8,8 @@ auto-loads (railtie + middleware) when Rails is present.
|
|
|
8
8
|
## Capabilities
|
|
9
9
|
|
|
10
10
|
- **Endpoint tracking** — every request/response passing through the Rack middleware is reported.
|
|
11
|
-
- **Authorization** — outbound calls to other EndPointBlank-protected services
|
|
12
|
-
|
|
11
|
+
- **Authorization** — outbound calls to other EndPointBlank-protected services carry a
|
|
12
|
+
`Bearer` access token (never this service's own client credentials), and inbound requests can be authorized
|
|
13
13
|
against the EndPointBlank service before your action runs.
|
|
14
14
|
- **Error, request, response, and log reporting** — background, queued, non-blocking delivery to
|
|
15
15
|
the EndPointBlank intake API.
|
|
@@ -61,29 +61,51 @@ EndPointBlank::Writers::LogWriter.info("service started", { pid: Process.pid })
|
|
|
61
61
|
|
|
62
62
|
## Configuration
|
|
63
63
|
|
|
64
|
-
`EndPointBlank.configure { |c| ... }` yields the `EndPointBlank::Configuration`
|
|
64
|
+
`EndPointBlank.configure { |c| ... }` yields the `EndPointBlank::Configuration` settings and
|
|
65
|
+
applies every assignment made inside the block together, only once the block returns without
|
|
66
|
+
raising -- a block that raises leaves the configuration exactly as it was before the call.
|
|
65
67
|
Every setting listed below can be set explicitly in that block, and most also fall back to an
|
|
66
68
|
`ENDPOINTBLANK_*` environment variable, then to a built-in default.
|
|
67
69
|
|
|
70
|
+
`c` is only valid for the duration of the block: once `configure` returns, whether the block
|
|
71
|
+
returned normally or raised, `c` is frozen, and every String, Array or Hash value it holds is
|
|
72
|
+
first replaced with its own frozen deep copy. A write made through a reference to `c` kept past
|
|
73
|
+
the block always raises `FrozenError` -- a reassignment (`saved.app_name = "x"`) because `c`
|
|
74
|
+
itself is frozen, and an in-place edit (`saved.masking_rules << rule`, `saved.app_name << "x"`,
|
|
75
|
+
editing a rule Hash in place) because the value it points to is frozen too, not just `c`. A read
|
|
76
|
+
that bypasses `c` -- `EndPointBlank::Configuration.instance.app_name`, or `EndPointBlank.logger`
|
|
77
|
+
right after `c.logger = ...` earlier in the same block -- still sees the value from before the
|
|
78
|
+
`configure` call started, not what the block has set on `c` so far, until the block returns and
|
|
79
|
+
the change is applied.
|
|
80
|
+
|
|
81
|
+
Assigning a String, Array or Hash through `c` copies it rather than storing the object itself:
|
|
82
|
+
after `c.masking_rules = rules`, mutating the `rules` array you passed in no longer affects the
|
|
83
|
+
live configuration -- call `configure` again to apply a further change. `logger`, `mask_hook`
|
|
84
|
+
and `version_finder` are not String/Array/Hash, so they are held by reference like any other
|
|
85
|
+
object the caller hands in: mutating one through a retained `c` (`saved.logger.level = ...`)
|
|
86
|
+
still reaches the live value, the same as mutating it through `EndPointBlank.logger` or
|
|
87
|
+
`Configuration.instance.logger` directly would.
|
|
88
|
+
|
|
68
89
|
**Precedence: explicit `configure` value > `ENDPOINTBLANK_*` environment variable > default.**
|
|
69
90
|
|
|
70
91
|
| `configure` setting | Env var fallback | Default | Notes |
|
|
71
92
|
|---|---|---|---|
|
|
72
|
-
| `client_id` | `ENDPOINTBLANK_CLIENT_ID` | `nil` |
|
|
93
|
+
| `client_id` | `ENDPOINTBLANK_CLIENT_ID` | `nil` | Authenticates this service to its own intake (`Basic`). Never sent to a provider. |
|
|
73
94
|
| `client_secret` | `ENDPOINTBLANK_CLIENT_SECRET` | `nil` | Paired with `client_id`. |
|
|
74
95
|
| `base_url` | `ENDPOINTBLANK_BASE_URL` | `https://in.endpointblank.com` | Base for access-token, authorize, and endpoint-update APIs. |
|
|
75
96
|
| `log_base_url` | `ENDPOINTBLANK_LOG_BASE_URL` | `https://log.endpointblank.com` | Base for error/request/response/log reporting APIs. |
|
|
76
97
|
| `app_name` | `ENDPOINTBLANK_APP_NAME` | `Rails.application.name.underscore` if Rails is defined, else `nil` | Identifies your app to EndPointBlank. |
|
|
77
98
|
| `env_name` | `ENDPOINTBLANK_ENV` | `RACK_ENV`, then `APP_ENV`, then `Rails.env` if defined, else `"production"` (resolved per-request by `SessionConfiguration.env_name`, not read directly off `Configuration`) | The environment name reported with each request/response payload. |
|
|
78
99
|
| `logger` | — | A `::Logger.new($stdout, level: ::Logger::INFO)`, or `Rails.logger` under Rails (set by the railtie) | Any object with `.debug`/`.info`/`.warn`/`.error`/`.fatal` works. |
|
|
79
|
-
| `worker_count` | — | `4` |
|
|
100
|
+
| `worker_count` | — | `4` | Number of background threads draining the delayed writer's queue. Falls back to 2 when set to `nil`. |
|
|
80
101
|
| `token_ttl` | — | `nil` | Optional TTL (seconds) requested when generating a `Bearer` access token. |
|
|
81
|
-
| `cache_ttl` | — | `300` | TTL
|
|
102
|
+
| `cache_ttl` | — | `300` | TTL, in whole seconds, for the authorization decision cache. **Omit it to get the 300-second default.** `0` disables the cache; any positive `Integer` is that many seconds. Anything else — an explicit `nil`, a negative number, or a non-`Integer` such as `"300"` or `3.5` — raises `ArgumentError` from the `c.cache_ttl = ...` assignment itself, at configure time, and leaves the previous value in place, so a bad value stops your app at boot instead of surfacing on the first authorized request. The cache is a plain in-memory Hash scoped to this process (no `Rails.cache`, Redis, or other shared store), so a Puma or Unicorn worker and every separate app instance each hold their own cache and their own view of `cache_ttl`. Changing `cache_ttl` at runtime takes effect immediately for already-cached entries, not just new ones: an entry is valid only while it is within *both* its original write-time expiry and the *currently configured* `cache_ttl` measured from when it was written, so raising `cache_ttl` never extends an entry already in the cache, and lowering it shortens one on its next read. `cache_ttl = 0` disables the cache — the moment a read or a store in a given process observes it disabled, *that process's* entire cache is cleared, not just the entry being looked up or written, and a store made while disabled inserts nothing there. This is per process, not fleet-wide: each worker or instance clears only its own cache, and only once it has itself observed `cache_ttl` disabled and then handled an `Authorized` request (or a direct cache call) while disabled — a worker that is idle, or hasn't yet picked up the new config, keeps serving whatever it already cached until it does. (A disable followed by a re-enable with no cache read or store in between flushes nothing, since nothing observed the disabled state.) |
|
|
82
103
|
| `trust_proxy_headers` | — | `true` | Whether the per-request `scheme`/`host`/`port` report honors `X-Forwarded-Proto`/`-Host`/`-Port`. See [Reported base URL](#reported-base-url). |
|
|
83
104
|
| `masking_rules` | — | `[]` | Ordered list of masking rule hashes — see [Data masking](#data-masking). |
|
|
84
105
|
| `mask_hook` | — | `nil` | Optional `->(payload, record_type_string) { payload }` run after `masking_rules`. |
|
|
85
106
|
| `version_finder` | — | `nil` | Optional `->(request) { "1" }` overriding `EndPointBlank::Commands::VersionFinder`'s default header/param/path detection. |
|
|
86
107
|
| `application_version` | — | `nil` | Reserved for reporting your app's own version. |
|
|
108
|
+
| `derive_base_url_from_client_id` | — | `false` | Derive the intake hostname from a slug-prefixed `client_id` when no `base_url` is set. See [Intake hostname from `client_id`](#intake-hostname-from-client_id). Only `true` or `false`; anything else raises `ArgumentError` at configure time. |
|
|
87
109
|
|
|
88
110
|
Note: there is also a bare `environment` accessor on `Configuration`, but it is not read by any
|
|
89
111
|
code path in this gem (the real per-request environment name is `env_name`, described above) — do
|
|
@@ -118,6 +140,42 @@ hostname on an internal port. `host` is caller-controlled either way (it has alw
|
|
|
118
140
|
the `Host` header), and none of these three values is ever used as an identity or
|
|
119
141
|
authorization key, so the worst case is a wrong *suggestion* that an admin has to approve.
|
|
120
142
|
|
|
143
|
+
### Intake hostname from `client_id`
|
|
144
|
+
|
|
145
|
+
Each organization's intake will answer at its own hostname,
|
|
146
|
+
`https://<slug>.in.endpointblank.com`, and every new `client_id` starts with
|
|
147
|
+
that slug and a dot (`acima-x7k2mq.ijXI+MVwmrC5xH/9ZuGiQlAbAyobTqMa`). With
|
|
148
|
+
`c.derive_base_url_from_client_id = true`, the gem picks its intake in this
|
|
149
|
+
order:
|
|
150
|
+
|
|
151
|
+
1. `base_url`, or else `ENDPOINTBLANK_BASE_URL`, if either is set;
|
|
152
|
+
2. else, if the `client_id` carries a slug prefix,
|
|
153
|
+
`https://<slug>.in.endpointblank.com`;
|
|
154
|
+
3. else `https://in.endpointblank.com`.
|
|
155
|
+
|
|
156
|
+
A `client_id` carries a slug prefix only when the part before its first `.`
|
|
157
|
+
has the exact shape of an organization slug and something follows the dot
|
|
158
|
+
(`EndPointBlank::Configuration.client_id_slug`). A credential issued before
|
|
159
|
+
slugs, including one with a `.` in it such as `my.client`, keeps calling
|
|
160
|
+
`https://in.endpointblank.com`.
|
|
161
|
+
|
|
162
|
+
**This is off by default, and turns on by default in a later release, once
|
|
163
|
+
DNS and TLS for `*.in.endpointblank.com` are live.** Until then those
|
|
164
|
+
hostnames do not resolve in production, so leave it off unless EndPointBlank
|
|
165
|
+
has told you otherwise. With it off, the base URL is `base_url`, else
|
|
166
|
+
`ENDPOINTBLANK_BASE_URL`, else `https://in.endpointblank.com`, whatever the
|
|
167
|
+
`client_id`.
|
|
168
|
+
|
|
169
|
+
The logs hostname is not derived: `log_base_url`, else
|
|
170
|
+
`ENDPOINTBLANK_LOG_BASE_URL`, else `https://log.endpointblank.com`, as before.
|
|
171
|
+
|
|
172
|
+
Every call to intake also sends `x-epb-sdk: ruby/<version>`, so
|
|
173
|
+
EndPointBlank can tell which SDK versions use a credential before it moves an
|
|
174
|
+
organization to another intake. The minimum Ruby version for a move is the
|
|
175
|
+
release that turns `derive_base_url_from_client_id` on by default, **not**
|
|
176
|
+
this one: with the option at its default here, the gem keeps calling
|
|
177
|
+
`https://in.endpointblank.com` after its organization has moved.
|
|
178
|
+
|
|
121
179
|
### `configure` block example
|
|
122
180
|
|
|
123
181
|
```ruby
|
|
@@ -150,27 +208,146 @@ export ENDPOINTBLANK_ENV=staging
|
|
|
150
208
|
|
|
151
209
|
### Authorization
|
|
152
210
|
|
|
153
|
-
`EndPointBlank::Authorization.header(base_url
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`client_id` / `client_secret`
|
|
157
|
-
be obtained.
|
|
211
|
+
`EndPointBlank::Authorization.header(base_url)` builds the `Authorization` header for an outbound
|
|
212
|
+
call to a provider. It is always a `Bearer` token covering `base_url` (via
|
|
213
|
+
`EndPointBlank::AccessTokens`, minting one if none is cached). It **never** falls back to `Basic`:
|
|
214
|
+
a client must never send its own `client_id` / `client_secret` to a provider or to the provider's
|
|
215
|
+
intake. When no token can be obtained it raises `EndPointBlank::TokenUnavailableError` instead.
|
|
158
216
|
|
|
159
217
|
```ruby
|
|
160
|
-
|
|
218
|
+
# Pass the URL you are about to call, NOT a hostname. Its userinfo, query and
|
|
219
|
+
# fragment are removed before the token request; they are never sent to
|
|
220
|
+
# intake, logged, or kept on the error.
|
|
221
|
+
url = "https://api.example.com/orders"
|
|
222
|
+
|
|
223
|
+
begin
|
|
224
|
+
auth = EndPointBlank::Authorization.header(url) # => "Bearer ..."
|
|
225
|
+
Excon.post(url, headers: { "Authorization" => auth }, body: payload)
|
|
226
|
+
rescue EndPointBlank::TokenUnavailableError => e
|
|
227
|
+
# No token, so the provider was never called. e.outcome / e.status /
|
|
228
|
+
# e.failure say why (see "Why a token could not be minted" below): retry,
|
|
229
|
+
# degrade, or fail your own request -- but do not send credentials instead.
|
|
230
|
+
Rails.logger.warn(e.message)
|
|
231
|
+
raise
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`base_url` is required. Until this release `header` with no argument returned `Basic` credentials; that
|
|
236
|
+
form is gone, and `header(nil)` or `header("")` raises `ArgumentError`, as does a URL that cannot be
|
|
237
|
+
parsed into an http or https URL with a host (nothing is sent, and the message does not repeat the URL). The SDK's own calls to its
|
|
238
|
+
own intake (authorize, token minting, endpoint updates, the log/request/response writers) still
|
|
239
|
+
authenticate with `Basic`, which is safe because intake already holds this service's credential;
|
|
240
|
+
they use the internal `EndPointBlank::Authorization.intake_header`, which is not for outbound
|
|
241
|
+
calls.
|
|
161
242
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
EndPointBlank::
|
|
243
|
+
`TokenUnavailableError` (a subclass of `EndPointBlank::Error`) carries `base_url` (the URL with its
|
|
244
|
+
userinfo, query and fragment removed; the raw value is never kept), `failure` (the
|
|
245
|
+
`EndPointBlank::AccessTokens::Failure` recorded for the mint, or `nil`), and the shortcuts
|
|
246
|
+
`outcome` and `status`. Its message names the stripped URL and a fixed reason for the outcome
|
|
247
|
+
-- never intake's response body, which stays on `failure.reason` -- for example:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Could not mint an EndPointBlank access token for https://api.example.com/orders:
|
|
251
|
+
intake rejected this application's client credential (HTTP 401); retrying cannot help --
|
|
252
|
+
re-issue the credential. EndPointBlank never sends this service's client_id/client_secret to a
|
|
253
|
+
provider, so there is no Basic-auth fallback and the call must not be made without a token.
|
|
165
254
|
```
|
|
166
255
|
|
|
256
|
+
| `outcome` | Reason in the message |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `:credential_rejected` | `intake rejected this application's client credential (HTTP 401); retrying cannot help -- re-issue the credential` |
|
|
259
|
+
| `:request_rejected` | `intake refused the token request (HTTP <status>); check the URL and that a grant covers the target` |
|
|
260
|
+
| `:server_error` | `intake failed to issue a token (HTTP <status>); this may be transient` |
|
|
261
|
+
| `:transport_error` | `intake could not be reached (timeout, connection refused or retries exhausted); this may be transient` |
|
|
262
|
+
| `:transport_error`, the mint raised | `the token request failed unexpectedly` |
|
|
263
|
+
| none recorded | `the token request failed for an unknown reason` |
|
|
264
|
+
|
|
265
|
+
` (HTTP <status>)` is left out when there is no status.
|
|
266
|
+
|
|
267
|
+
A mint that raises rather than reporting a failure -- a bug, not intake being unreachable -- is
|
|
268
|
+
reported as this error too, with outcome `:transport_error`, `unexpected?` true and the exception
|
|
269
|
+
as `cause`; its message is not copied into the error's. Only a missing credential's
|
|
270
|
+
`ConfigurationError` escapes `header` as itself.
|
|
271
|
+
|
|
272
|
+
The SDK's own calls to its intake raise `EndPointBlank::ConfigurationError` (also a subclass of
|
|
273
|
+
`EndPointBlank::Error`) when `client_id` or `client_secret` is missing or empty, rather than
|
|
274
|
+
sending an empty `Basic` credential.
|
|
275
|
+
|
|
167
276
|
The argument is the URL you are about to call. intake matches it against registered base URLs by
|
|
168
277
|
longest path prefix, so you need not know how the target registered itself -- `header` for
|
|
169
278
|
`https://api.example.com/orders/42` reuses a token already cached for
|
|
170
279
|
`https://api.example.com/orders`. `EndPointBlank::AccessTokens` caches one token per base URL
|
|
171
280
|
intake resolves to, not one per process, so a service that calls several targets holds a token
|
|
172
|
-
for each.
|
|
173
|
-
|
|
281
|
+
for each. The lookup uses the URL with its userinfo, query and fragment removed and its scheme and
|
|
282
|
+
host lowercased, as intake does; beyond that, a URL that does not match character-for-character
|
|
283
|
+
(a different path case, an unregistered path) simply misses and mints a new token -- it never guesses.
|
|
284
|
+
|
|
285
|
+
### Why a token could not be minted
|
|
286
|
+
|
|
287
|
+
`EndPointBlank::AccessTokens.token` answers with a token String or `nil` (or raises, as itself,
|
|
288
|
+
anything the mint raised that is not a transport error -- only `Authorization.header` wraps that),
|
|
289
|
+
which is all most callers need. When `nil` is not enough — when you want to know whether retrying could possibly
|
|
290
|
+
help — call `token_result` instead, which answers with the token or the `Failure` for that call:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
url = "https://api.example.com/orders"
|
|
294
|
+
result = EndPointBlank::AccessTokens.token_result(url)
|
|
295
|
+
|
|
296
|
+
if result.is_a?(EndPointBlank::AccessTokens::Failure)
|
|
297
|
+
failure = result
|
|
298
|
+
|
|
299
|
+
case failure.outcome
|
|
300
|
+
when :credential_rejected
|
|
301
|
+
# intake answered 401. Permanent until the credential itself changes:
|
|
302
|
+
# re-issue it in the portal and update client_id / client_secret.
|
|
303
|
+
raise "EndPointBlank credential rejected (#{failure.reason})"
|
|
304
|
+
when :request_rejected
|
|
305
|
+
# A 400 or 422: intake could not resolve the target or source
|
|
306
|
+
# application, or the request itself was malformed. Retrying will not fix
|
|
307
|
+
# it, but the credential is fine.
|
|
308
|
+
when :server_error, :transport_error
|
|
309
|
+
# A 5xx, an unusable response, a timeout, a refused connection. Try again.
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`EndPointBlank::AccessTokens.last_failure(url)` still answers the last failure recorded for a
|
|
315
|
+
URL, and returns `nil` once a mint for that URL succeeds again, so it never reports a problem
|
|
316
|
+
that has already cleared. It is a shared slot, though: read after `token` returned `nil`, another
|
|
317
|
+
thread may already have cleared or replaced it. When you need the reason for your own call, use
|
|
318
|
+
`token_result`, whose `Failure` is captured inside the cache's lock for that call. A `Failure` carries `base_url`, `outcome`, `status` (the HTTP
|
|
319
|
+
status, or `nil` when no usable one was obtained), `reason`, and `at`, and answers
|
|
320
|
+
`#credential_rejected?`, `#request_rejected?`, `#server_error?` and `#transport_error?`.
|
|
321
|
+
|
|
322
|
+
`token`, `token_result`, `exists?` and `last_failure` all remove the URL's userinfo, query and
|
|
323
|
+
fragment first, so the cache, the failure record (and its `base_url`) and the log lines only ever
|
|
324
|
+
hold the stripped URL. A URL that cannot be parsed into an http or https URL with a host is never sent:
|
|
325
|
+
`token_result` answers a `:request_rejected` `Failure` with no `status` and no `base_url`, and
|
|
326
|
+
nothing is recorded.
|
|
327
|
+
|
|
328
|
+
There is deliberately no `#retriable?` or other single retry/no-retry boolean. Retrying a `400`
|
|
329
|
+
or a `422` is exactly as futile as retrying a `401` — intake answers `400` for an invalid
|
|
330
|
+
`token_ttl` or a missing `base_url`, and `422` when the target or source application cannot be
|
|
331
|
+
resolved — so a boolean would have to answer for cases whose only honest answer is "it depends
|
|
332
|
+
what you are going to do about it". Branch on the outcome instead.
|
|
333
|
+
|
|
334
|
+
Classification is on the HTTP status first and the body second. A `401` whose body will not parse
|
|
335
|
+
is still `:credential_rejected`: the SDK reaches intake through a proxy, and a WAF or load
|
|
336
|
+
balancer can answer 401 with an HTML page intake never generated. `:transport_error` means one
|
|
337
|
+
thing only — no usable HTTP status was obtained because the request never completed (an Excon,
|
|
338
|
+
socket, SSL or timeout error). Anything else raised while minting is not a transport error and
|
|
339
|
+
propagates.
|
|
340
|
+
|
|
341
|
+
The body decides exactly one thing, and only on a 2xx: whether a token was actually minted. A
|
|
342
|
+
success means a token is there to read — the body parsed and carries a non-empty `token` and the
|
|
343
|
+
non-empty `base_url` to cache it under. A 2xx that will not parse, or carries no token, or a
|
|
344
|
+
token with no `base_url`, is a `:server_error` keeping its real 2xx status, because intake's
|
|
345
|
+
`base_url` is `NOT NULL` and it answers a 4xx rather than minting when the URL resolves to
|
|
346
|
+
nothing — so a 2xx missing one is a broken server, not a refused request.
|
|
347
|
+
|
|
348
|
+
The same verdict is available one level down, without the cache, from
|
|
349
|
+
`EndPointBlank::Commands::GenerateAccessToken.token_result(base_url)`, which returns an
|
|
350
|
+
`AccessTokenResult` with `outcome`, `status`, `payload` and the same predicates.
|
|
174
351
|
|
|
175
352
|
Under Rails, protect an inbound endpoint by including the `Authorized` concern in a controller —
|
|
176
353
|
it calls `EndPointBlank::Commands::EndpointAuthorize.authorize(request)` before the action, and
|
|
@@ -197,6 +374,32 @@ preserve it) and you registered the external hostname in the portal, either upda
|
|
|
197
374
|
registered hostname to the internal one the app now reports, or configure the proxy to preserve
|
|
198
375
|
`Host`. Deployments where `Host` and `X-Forwarded-Host` agree are unaffected.
|
|
199
376
|
|
|
377
|
+
The `Authenticated` concern is the lighter sibling: it calls
|
|
378
|
+
`EndPointBlank::Commands::BasicAuthenticate.authenticate(request)` before the action and refuses
|
|
379
|
+
the same way, but records nothing on the Rack env, sets no deprecation headers and — matching
|
|
380
|
+
every other SDK's authenticate path — does not cache intake's answer.
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
class OrdersController < ApplicationController
|
|
384
|
+
include EndPointBlank::Rails::Authenticated
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`EndPointBlank::UnauthorizedError#status` is intake's own verdict, from either concern, so
|
|
389
|
+
`rescue_from` can render it directly. 401 and 403 are different remedies and must not be
|
|
390
|
+
collapsed:
|
|
391
|
+
|
|
392
|
+
| intake answered | `error.status` |
|
|
393
|
+
| --- | --- |
|
|
394
|
+
| 401 | `401` — re-check or re-issue the credential |
|
|
395
|
+
| 403 | `403` — ask for a grant covering this endpoint |
|
|
396
|
+
| any other non-201 | that status, verbatim |
|
|
397
|
+
| nothing at all | `503` — the check could not be made, so nothing judged this caller |
|
|
398
|
+
|
|
399
|
+
`UnauthorizedError.new(message)` still defaults to 401; the status is an optional second
|
|
400
|
+
argument. Raise the instance (`raise UnauthorizedError.new(msg, status)`) rather than
|
|
401
|
+
`raise UnauthorizedError, msg` — the two-argument form cannot carry a status.
|
|
402
|
+
|
|
200
403
|
### Error reporting
|
|
201
404
|
|
|
202
405
|
Exceptions raised while `EndPointBlank::Middleware::Rack::ReportInteraction` is on the stack are
|
|
@@ -226,8 +429,21 @@ EndPointBlank::Writers::LogWriter.fatal("out of workers")
|
|
|
226
429
|
|
|
227
430
|
All writers (`RequestWriter`, `ResponseWriter`, `ExceptionWriter`, `LogWriter`) enqueue their
|
|
228
431
|
payload onto a bounded, in-memory queue (`DelayedWriter`, capacity 1000, drop-oldest under
|
|
229
|
-
sustained backpressure) drained by
|
|
230
|
-
|
|
432
|
+
sustained backpressure) drained by `worker_count` background threads that POST batches via `excon`,
|
|
433
|
+
six payloads per request. Batches are cut by position, so two identical payloads are two payloads.
|
|
434
|
+
Delivery is fire-and-forget and never raises into your request cycle.
|
|
435
|
+
|
|
436
|
+
A worker thread does not die. A batch can be lost — the intake may be unreachable, or the send path
|
|
437
|
+
may raise something nobody anticipated — but the loop catches every `StandardError`, logs it at
|
|
438
|
+
`error` level through `EndPointBlank.logger` with a running count of consecutive failures, backs off
|
|
439
|
+
(0.1s, doubling, capped at 30s), and keeps draining; the count resets on the first clean pass. Only
|
|
440
|
+
an error outside `StandardError` — `SystemExit`, `Interrupt`, `SignalException`, `NoMemoryError`,
|
|
441
|
+
i.e. the process itself going down — ends a worker.
|
|
442
|
+
|
|
443
|
+
A writer may optionally define `on_success(response)` and `on_failure(response)` to hear about each
|
|
444
|
+
batch. `on_failure` receives `nil` when the intake never answered at all: `Commands::Http` returns
|
|
445
|
+
`nil` once its three attempts are exhausted, which is the absence of a status rather than a failing
|
|
446
|
+
one. A writer that defines neither is unaffected.
|
|
231
447
|
|
|
232
448
|
### Data masking
|
|
233
449
|
|
|
@@ -290,6 +506,7 @@ end
|
|
|
290
506
|
|
|
291
507
|
class OrdersController < ApplicationController
|
|
292
508
|
include EndPointBlank::Rails::Authorized # authorize inbound requests before each action
|
|
509
|
+
# or: include EndPointBlank::Rails::Authenticated # authenticate only, no caching
|
|
293
510
|
include EndPointBlank::Rails::Versioned
|
|
294
511
|
|
|
295
512
|
version ["v1", "v2"], only: [:index]
|
data/end_point_blank.gemspec
CHANGED
|
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Robert A. Lasch"]
|
|
9
9
|
spec.email = ["rlasch@gmail.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Ruby
|
|
12
|
-
spec.description = "
|
|
13
|
-
spec.homepage = "https://
|
|
11
|
+
spec.summary = "Ruby and Rails SDK for EndPointBlank: authorize service-to-service API calls, report endpoint versions, and see which clients still call deprecated API versions."
|
|
12
|
+
spec.description = "Ruby and Rails SDK for EndPointBlank: authorize service-to-service (machine-to-machine) API calls, report endpoint versions, and see which clients still call deprecated API versions before you sunset them. A framework-agnostic core runs in plain Ruby / Sinatra, with Rails supported as an auto-loaded adapter."
|
|
13
|
+
spec.homepage = "https://endpointblank.com"
|
|
14
14
|
spec.required_ruby_version = ">= 3.4.2"
|
|
15
15
|
spec.license = "Nonstandard"
|
|
16
16
|
|
|
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
20
|
spec.metadata["source_code_uri"] = "https://github.com/EndPointBlank/end_point_blank_rails"
|
|
21
|
+
spec.metadata["documentation_uri"] = "https://endpointblank.com/docs/sdk-setup"
|
|
22
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/EndPointBlank/end_point_blank_rails/issues"
|
|
21
23
|
spec.metadata["changelog_uri"] = "https://github.com/EndPointBlank/end_point_blank_rails/releases"
|
|
22
24
|
|
|
23
25
|
# Specify which files should be added to the gem when it is released.
|