standard_id 0.35.0 → 0.36.1
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 +56 -0
- data/README.md +9 -2
- data/app/controllers/standard_id/api/base_controller.rb +31 -0
- data/app/controllers/standard_id/api/oauth/revocations_controller.rb +18 -1
- data/lib/standard_id/api/authentication_guard.rb +9 -3
- data/lib/standard_id/errors.rb +25 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id/web/authentication_guard.rb +33 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5519e19cd0f121d012683a24a10d0052ce4bf33e508ede831712db3851abace9
|
|
4
|
+
data.tar.gz: 9786b159f6ad07238cb7e636f3cfd5e028ec31cb6014f38e0349de3760f0deaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b18d51d0fc882c6aaf4c9a6482e58600bf82594860715c1b6f5850dd0175834f72dc5735dbb23b2e1f45fbc4ceb8f975dffdc2484a3ee1871b882e89d76c4df6
|
|
7
|
+
data.tar.gz: 7039cfdf1894c351a3802b893e31a2ff23754acd0a861116b190ca5875540a7ba45d35589cefef79ad1a65444feca3eb999740285bc5af0bd073139d41fdedbc
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.36.1] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
**A follow-up to 0.36.0's own regression, released the same day.** 0.36.0 is correct and stays — read its entry first for why `SESSION_VALIDATING` had to start carrying `account:`. This release repairs the fallout of that fix on the gem's own API routes. Two releases land together because the second is only reachable *because* of the first.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`StandardId::Api::BaseController` now answers `401` — not `500` — when the authenticated account is deactivated or locked.** 0.36.0 made `AccountStatus` / `AccountLocking` fire on a live authenticated request for the first time, which is the intended behaviour. But `AccountDeactivatedError` and `AccountLockedError` are bare `StandardError` subclasses, under neither `InvalidSessionError` nor `OAuthError`, and the API base controller rescued only those three families. So the very change that started refusing a disabled account's bearer token turned every route under `ApiEngine` — `/api/v1/sessions`, `/api/v1/userinfo`, all of it — into an unhandled exception for that account. Every consumer mounting `ApiEngine` inherited it. Before 0.36.0 the guard was inert on that path, so the hole existed but was unreachable; the release is what exposed it.
|
|
17
|
+
|
|
18
|
+
Observed independently in two consumers during the 0.36.0 rollout: `luminality-web`, which has an app-side handler on `Api::ErrorHandling` and so saw only the *gem-owned* routes fail, and `sidekick-web`, which has none and saw its whole API tree fail.
|
|
19
|
+
|
|
20
|
+
A 500 is not a refusal. It carries no `WWW-Authenticate`, tells the client nothing about discarding a token that will never work again, and pages the on-call for a routine account state. Both errors now render the established bearer shape — `401`, `{ "error": "invalid_token", "error_description": … }`, plus the matching `WWW-Authenticate` challenge — via the same `render_bearer_unauthorized!` the other credential failures use. No new response shape was invented.
|
|
21
|
+
|
|
22
|
+
**Why `invalid_token`.** RFC 6750 §3.1 defines exactly three codes. `invalid_token` covers a token "expired, revoked, malformed, or **invalid for other reasons**", and a token whose subject account has been disabled is invalid for one of those other reasons; it also carries the right client instruction — discard the token and re-authenticate — which is precisely correct here, since no retry with this token can ever succeed. `insufficient_scope` (403) would be wrong twice over: nothing about this is a scope failure, and 403 invites a client to keep the token and retry. `invalid_request` (400) would be wrong because the request is perfectly well-formed.
|
|
23
|
+
|
|
24
|
+
`AccountLockedError#lock_reason` is **not** surfaced. It is operator-authored text meant for logs and admin screens, and `WWW-Authenticate` is a quoted string that arbitrary text would break as well as leak. A spec pins that the reason appears in neither the body nor the header.
|
|
25
|
+
|
|
26
|
+
### Unchanged, deliberately — two decisions worth knowing about
|
|
27
|
+
|
|
28
|
+
- **The `WebEngine`'s own routes (`/sessions`, `/account`, `/logout`) get no gem-side handler, and this is not an oversight.** `StandardId::Web::BaseController` descends from the **host's** `ApplicationController`, so the `rescue_from StandardId::AccountDeactivatedError` the README has always told hosts to write already covers those routes. A gem handler there would not add safety, it would remove it: Rails scans `rescue_handlers` most-recently-registered-first, and a subclass registers after its parent, so anything the gem registered on `Web::BaseController` would outrank and silently override the host's — replacing a host's "your account has been deactivated" page with whatever the gem chose. `Api::BaseController` has no such escape hatch; descending from `ActionController::API`, there is nowhere in its ancestry for a host to put a handler, which is exactly why only that side is rescued in the gem.
|
|
29
|
+
|
|
30
|
+
**Hosts must therefore still carry their own `ApplicationController` handler for these two errors.** If you do not have one, a deactivated or locked account hitting a web route — yours or the engine's — is an unhandled exception. That was true before 0.36.0 for the sign-in path and remains true; 0.36.0 simply widened the set of requests that can reach it from "sign-in and token mint" to "any authenticated request". The README's account sections now spell out which routes the engine answers for and which it does not.
|
|
31
|
+
|
|
32
|
+
- **`AccountDeactivatedError` / `AccountLockedError` still descend from `StandardError`, not from `InvalidSessionError`.** Reparenting them was considered as the broader fix — it would make every consumer's existing `rescue_from StandardId::InvalidSessionError` catch them automatically, with no consumer change at all — and rejected on two grounds. First, they mean something genuinely different: an `InvalidSessionError` says the credential is no good and the remedy is to sign in again, while these say the credential is fine and the account is disabled, where signing in again will not help and the user needs to be told so. Consumers who deliberately distinguish the two would lose that distinction silently. Second, and decisively, it would hijack handlers hosts have already written: `Web::BaseController` registers `rescue_from NotAuthenticatedError, InvalidSessionError, with: :redirect_unauthenticated_to_login`, so under the reparented hierarchy a host's `rescue_from AccountDeactivatedError` — registered *earlier*, on the parent — would lose to the gem's subclass registration, and every consumer following the README would start bouncing disabled users to `/login` instead of to their account-disabled page. A patch release that quietly re-routes a documented UX is not a patch. The narrow fix leaves both hierarchies and both sets of host handlers exactly as they were.
|
|
33
|
+
|
|
34
|
+
### Testing
|
|
35
|
+
|
|
36
|
+
`spec/requests/standard_id/live_session_account_guard_spec.rb` gains three API examples (401 shape for deactivated, 401 shape for locked, and `lock_reason` non-disclosure) and two web examples pinning the delegate-to-host design above. Proven by negative control: with the two `rescue_from` lines deleted from `Api::BaseController`, the three API examples fail with the raw error escaping the request; restored, the file is 14/14 and the suite 2097/2097.
|
|
37
|
+
|
|
38
|
+
Relates to rarebit-one/rarebit-ops#306.
|
|
39
|
+
|
|
40
|
+
## [0.36.0] - 2026-08-04
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
|
|
44
|
+
- **`SESSION_VALIDATING` now carries the `account:` its subscribers guard on, so `AccountStatus` and `AccountLocking` finally stop a LIVE authenticated request.** Both concerns subscribe to `OAUTH_TOKEN_ISSUING`, `SESSION_CREATING` and `SESSION_VALIDATING` and branch on `event[:account]&.inactive?` / `&.locked?`. Neither publisher of `SESSION_VALIDATING` ever sent an `account:` — `Web::AuthenticationGuard#emit_session_validating` and `Api::AuthenticationGuard#emit_session_validating` both published `session:` alone. `Events#enrich_payload` injects `:current_account`, a *different* key, so `event[:account]` was `nil` and that leg of both guards never fired.
|
|
45
|
+
|
|
46
|
+
That it was an omission rather than a design is plain from the sibling emitters in the same two files: `emit_session_validated` and `emit_session_expired` each pass `account:`. Only the *validating* hook — the one that runs before the request is allowed to proceed, and therefore the only one of the three that can stop it — left it out.
|
|
47
|
+
|
|
48
|
+
**Upgrade implication — expect previously-working sessions and tokens to start failing. That is the fix.** Three shapes were getting through and now do not:
|
|
49
|
+
|
|
50
|
+
1. **API bearer tokens, the big one.** `AccountStatusSubscriber` / `AccountLockingSubscriber` revoke `account.sessions.active` on `deactivate!` / `lock!`, but an access token is a stateless JWT with no `Session` row — revoking sessions does nothing to it. Until its `exp`, `SESSION_VALIDATING` was the only thing that could refuse it, and it was inert. Any client holding an access token minted before the account went bad kept working; it now gets `AccountDeactivatedError` / `AccountLockedError` on the next request.
|
|
51
|
+
2. **A status change that skips the callbacks** — `update_all`, `update_column`, a data migration, direct SQL, an admin bulk action. No `ACCOUNT_DEACTIVATED` / `ACCOUNT_LOCKED` event fires, so no session revocation happens, and the browser session stayed live until it expired.
|
|
52
|
+
3. **A session minted after the account went bad by a path that emits no `SESSION_CREATING`.** `Web::SessionManager#load_session_from_remember_token` calls `create_browser_session` directly and publishes nothing, so remember-me re-auth walked straight past the sign-in guard.
|
|
53
|
+
|
|
54
|
+
Hosts that lock or deactivate accounts should expect support traffic from users whose sessions used to survive. `sidekick-web`'s `LockAccountModal` copy — *"The user will be immediately locked out / All active sessions will be terminated"* — is now true rather than aspirational.
|
|
55
|
+
|
|
56
|
+
**No new query on either hot path.** The web guard reads the `:account` association only when it is already loaded (`SessionManager` loads the session with `eager_load(:account)`) and otherwise falls back to a single indexed `find_by` — it must never lazily touch the association, because several consumers run `strict_loading_by_default` and this emitter is on the path of *every* authenticated request, where a `StrictLoadingViolationError` would turn an inert guard into a 500. It also runs *before* the blank/expired/revoked checks, so it handles a nil session. The API guard threads its `SessionManager` through to reuse the memoized, strict-loading-cleared `#current_account` that `emit_session_validated` resolves anyway.
|
|
57
|
+
|
|
58
|
+
The spec that let this survive for so long asserted enforcement by publishing `SESSION_VALIDATING` **by hand** with the very key the real publisher omitted — proof that the subscriber reacts to a correctly-shaped payload, not that any caller produces one. Those examples now drive the real guards over real HTTP requests (`spec/requests/standard_id/live_session_account_guard_spec.rb`), and were confirmed to fail with the `account:` key removed.
|
|
59
|
+
|
|
60
|
+
Tracked in rarebit-one/rarebit-ops#306.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **`POST /oauth/revoke` logs a warning when `revocation_scope: :grant` resolves a presented `jti` to no `RefreshToken`.** Under `:grant`, presenting an access token is a deliberate no-op that still answers 200 (RFC 7009 §2.2) — correct, because access tokens are stateless and never persisted, but until now completely silent. A client that only ever presents access tokens looked like it was revoking when it was not. As `:grant` is adopted across the estate, post-flip no-ops need to be visible. Emitted via `StandardId.logger&.warn`, consistent with the existing `revocation_scope` fallback warning. No behaviour change. Relates to rarebit-one/rarebit-ops#304.
|
|
65
|
+
|
|
10
66
|
## [0.35.0] - 2026-07-31
|
|
11
67
|
|
|
12
68
|
### Security
|
data/README.md
CHANGED
|
@@ -784,7 +784,14 @@ User.inactive # => Users with status 'inactive'
|
|
|
784
784
|
|
|
785
785
|
### Handling AccountDeactivatedError
|
|
786
786
|
|
|
787
|
-
When an inactive account attempts to authenticate, `StandardId::AccountDeactivatedError` is raised. You need to handle this error in your application controller:
|
|
787
|
+
When an inactive account attempts to authenticate — at sign-in, at token mint, and (since 0.36.0) on every request made with an already-issued credential — `StandardId::AccountDeactivatedError` is raised. You need to handle this error in your application controller:
|
|
788
|
+
|
|
789
|
+
> **What the engine's own routes do.** You are responsible for **your** controllers only. The engine handles its own:
|
|
790
|
+
>
|
|
791
|
+
> - **`ApiEngine` routes** (`/api/v1/sessions`, `/api/v1/userinfo`, …) answer **`401` with `error: "invalid_token"`** per RFC 6750 — `StandardId::Api::BaseController` descends from `ActionController::API`, so your `rescue_from` is not in its ancestry and could never have covered it.
|
|
792
|
+
> - **`WebEngine` routes** (`/sessions`, `/account`, `/logout`) are covered by the `rescue_from` you write on `ApplicationController` below, because `StandardId::Web::BaseController` descends from it. The engine deliberately registers no handler of its own there — one would take precedence over yours and override your UX.
|
|
793
|
+
>
|
|
794
|
+
> So: write the `ApplicationController` handler (it covers your pages *and* the web engine's), and write the `Api::BaseController` handler only for **your own** API controllers.
|
|
788
795
|
|
|
789
796
|
```ruby
|
|
790
797
|
# app/controllers/application_controller.rb
|
|
@@ -900,7 +907,7 @@ User.unlocked.active # => Users who can log in
|
|
|
900
907
|
|
|
901
908
|
### Handling AccountLockedError
|
|
902
909
|
|
|
903
|
-
When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock:
|
|
910
|
+
When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock. The same engine-owned-route rules described under [Handling AccountDeactivatedError](#handling-accountdeactivatederror) apply: `ApiEngine` routes answer `401 invalid_token` themselves (never echoing `lock_reason`), `WebEngine` routes defer to your `ApplicationController` handler.
|
|
904
911
|
|
|
905
912
|
```ruby
|
|
906
913
|
# app/controllers/application_controller.rb
|
|
@@ -16,6 +16,16 @@ module StandardId
|
|
|
16
16
|
rescue_from StandardId::InvalidSessionError, with: :handle_invalid_session
|
|
17
17
|
rescue_from StandardId::OAuthError, with: :handle_oauth_error
|
|
18
18
|
|
|
19
|
+
# AccountStatus / AccountLocking raise these from the SESSION_VALIDATING
|
|
20
|
+
# subscriber, i.e. mid-request on an ALREADY authenticated call, not just
|
|
21
|
+
# at sign-in or token mint. Unrescued they are a 500 on every gem-owned
|
|
22
|
+
# API route. Unlike Web::BaseController — which descends from the host's
|
|
23
|
+
# ApplicationController, so a host `rescue_from` covers it — this class
|
|
24
|
+
# descends from ActionController::API, so the host has nowhere to put a
|
|
25
|
+
# handler in the ancestry and the gem must answer for itself.
|
|
26
|
+
rescue_from StandardId::AccountDeactivatedError, with: :handle_account_deactivated
|
|
27
|
+
rescue_from StandardId::AccountLockedError, with: :handle_account_locked
|
|
28
|
+
|
|
19
29
|
protected
|
|
20
30
|
|
|
21
31
|
def validate_content_type!
|
|
@@ -43,6 +53,27 @@ module StandardId
|
|
|
43
53
|
render_bearer_unauthorized!(error_description: default_invalid_token_message)
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
# RFC 6750 §3.1 offers exactly three error codes, and `invalid_token` is
|
|
57
|
+
# the right one: it covers a token "expired, revoked, malformed, or
|
|
58
|
+
# invalid for other reasons", and a bearer token whose subject account has
|
|
59
|
+
# been disabled is invalid for one of those other reasons. It also carries
|
|
60
|
+
# the correct client instruction — discard the token and re-authenticate —
|
|
61
|
+
# which is what we want, since no retry with this token will ever succeed.
|
|
62
|
+
# `insufficient_scope` (403) would be wrong: nothing here is about scope,
|
|
63
|
+
# and a 403 invites the client to keep the token and retry. `invalid_request`
|
|
64
|
+
# (400) would be wrong: the request is well-formed.
|
|
65
|
+
def handle_account_deactivated(_error)
|
|
66
|
+
render_bearer_unauthorized!(error_description: "The account is deactivated")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Deliberately does NOT surface `error.lock_reason`. It is operator-authored
|
|
70
|
+
# text intended for logs and admin screens (see StandardId::AccountLockedError),
|
|
71
|
+
# and the WWW-Authenticate header this renders into is a quoted string that
|
|
72
|
+
# arbitrary text would break as well as leak.
|
|
73
|
+
def handle_account_locked(_error)
|
|
74
|
+
render_bearer_unauthorized!(error_description: "The account is locked")
|
|
75
|
+
end
|
|
76
|
+
|
|
46
77
|
def handle_oauth_error(error)
|
|
47
78
|
render json: {
|
|
48
79
|
error: error.oauth_error_code,
|
|
@@ -93,10 +93,27 @@ module StandardId
|
|
|
93
93
|
.where(token_digest: StandardId::RefreshToken.digest_for(jti))
|
|
94
94
|
.includes(:session, :account)
|
|
95
95
|
.first
|
|
96
|
+
# No persisted grant for this jti. Per RFC 7009 §2.2 the endpoint
|
|
97
|
+
# still answers 200, and for an access token that is the correct
|
|
98
|
+
# no-op (access tokens are stateless JWTs and are never persisted).
|
|
99
|
+
# But under :grant the no-op is otherwise completely silent, so a
|
|
100
|
+
# client that only ever presents access tokens looks like it is
|
|
101
|
+
# revoking when it is not. Log it so post-flip no-ops are visible.
|
|
102
|
+
if record.nil?
|
|
103
|
+
StandardId.logger&.warn(
|
|
104
|
+
"[StandardId::Revocations] revocation_scope=:grant — presented jti " \
|
|
105
|
+
"resolved to no RefreshToken for account #{payload[:sub]}; nothing " \
|
|
106
|
+
"revoked (RFC 7009 §2.2 still returns 200). This is expected when " \
|
|
107
|
+
"an access token is presented; present the refresh token for " \
|
|
108
|
+
"revocation to take effect."
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
96
113
|
# A jti that resolves to another subject's grant must never revoke
|
|
97
114
|
# it — the token was signature-verified, but bind the two identities
|
|
98
115
|
# anyway rather than trusting a single claim.
|
|
99
|
-
return if record.
|
|
116
|
+
return if record.account_id.to_s != payload[:sub].to_s
|
|
100
117
|
|
|
101
118
|
refresh_tokens_revoked = record.revoke_family!
|
|
102
119
|
|
|
@@ -3,7 +3,7 @@ module StandardId
|
|
|
3
3
|
class AuthenticationGuard
|
|
4
4
|
def require_session!(session_manager, request: nil)
|
|
5
5
|
api_session = session_manager.current_session
|
|
6
|
-
emit_session_validating(api_session, request)
|
|
6
|
+
emit_session_validating(api_session, request, session_manager: session_manager)
|
|
7
7
|
|
|
8
8
|
if api_session.blank?
|
|
9
9
|
raise StandardId::NotAuthenticatedError, "Invalid or missing access token"
|
|
@@ -55,10 +55,16 @@ module StandardId
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
# Mirrors emit_session_validated/expired below. The session_manager is
|
|
59
|
+
# threaded through so resolve_account takes its memoized, strict-loading-
|
|
60
|
+
# cleared #current_account rather than a lazy `api_session.account` —
|
|
61
|
+
# this emitter runs on every authenticated request, before the
|
|
62
|
+
# blank/expired/revoked checks, so it must not be able to raise.
|
|
63
|
+
def emit_session_validating(api_session, request, session_manager: nil)
|
|
59
64
|
StandardId::Events.publish(
|
|
60
65
|
StandardId::Events::SESSION_VALIDATING,
|
|
61
|
-
session: api_session
|
|
66
|
+
session: api_session,
|
|
67
|
+
account: resolve_account(api_session, session_manager)
|
|
62
68
|
)
|
|
63
69
|
end
|
|
64
70
|
|
data/lib/standard_id/errors.rb
CHANGED
|
@@ -7,6 +7,31 @@ module StandardId
|
|
|
7
7
|
class RevokedSessionError < InvalidSessionError; end
|
|
8
8
|
|
|
9
9
|
# Account errors
|
|
10
|
+
#
|
|
11
|
+
# These deliberately do NOT descend from InvalidSessionError, even though
|
|
12
|
+
# doing so would make every host's existing `rescue_from InvalidSessionError`
|
|
13
|
+
# catch them for free. Two reasons:
|
|
14
|
+
#
|
|
15
|
+
# 1. They mean something different. An InvalidSessionError says the
|
|
16
|
+
# credential is no good — expired, revoked — and the remedy is to sign in
|
|
17
|
+
# again. These say the credential is fine and the ACCOUNT is disabled;
|
|
18
|
+
# signing in again will not help, and the user needs to be told so.
|
|
19
|
+
# Hosts document that distinction in their UX (see the README's
|
|
20
|
+
# "Handling AccountDeactivatedError" / "Handling AccountLockedError").
|
|
21
|
+
# 2. Reparenting them would silently HIJACK the handlers hosts already
|
|
22
|
+
# wrote. Web::BaseController registers
|
|
23
|
+
# `rescue_from NotAuthenticatedError, InvalidSessionError, with:
|
|
24
|
+
# :redirect_unauthenticated_to_login`, and Rails resolves rescue_from
|
|
25
|
+
# handlers most-recently-registered-first — a subclass's registration
|
|
26
|
+
# wins over its parent's. A host's `rescue_from AccountDeactivatedError`
|
|
27
|
+
# on ApplicationController is registered EARLIER than the gem's
|
|
28
|
+
# subclass registration, so as a subclass of InvalidSessionError these
|
|
29
|
+
# would start bouncing to /login instead of reaching the host's
|
|
30
|
+
# account-deactivated page. That is a behaviour regression for every
|
|
31
|
+
# consumer who followed the README.
|
|
32
|
+
#
|
|
33
|
+
# Consequently every entry point that must not raise has to rescue these
|
|
34
|
+
# explicitly; StandardId::Api::BaseController does.
|
|
10
35
|
class AccountDeactivatedError < StandardError; end
|
|
11
36
|
|
|
12
37
|
class AccountLockedError < StandardError
|
data/lib/standard_id/version.rb
CHANGED
|
@@ -27,7 +27,8 @@ module StandardId
|
|
|
27
27
|
def emit_session_validating(browser_session, request)
|
|
28
28
|
StandardId::Events.publish(
|
|
29
29
|
StandardId::Events::SESSION_VALIDATING,
|
|
30
|
-
session: browser_session
|
|
30
|
+
session: browser_session,
|
|
31
|
+
account: resolve_account(browser_session)
|
|
31
32
|
)
|
|
32
33
|
end
|
|
33
34
|
|
|
@@ -47,6 +48,37 @@ module StandardId
|
|
|
47
48
|
expired_at: browser_session.expires_at
|
|
48
49
|
)
|
|
49
50
|
end
|
|
51
|
+
|
|
52
|
+
# SESSION_VALIDATING is the only one of the three emitters that runs
|
|
53
|
+
# BEFORE the blank/expired/revoked checks, so it cannot borrow the
|
|
54
|
+
# assumptions its siblings rely on:
|
|
55
|
+
#
|
|
56
|
+
# * the session may be nil (the blank? check has not run yet), so a
|
|
57
|
+
# bare `browser_session.account` would NoMethodError; and
|
|
58
|
+
# * it also fires for sessions the host app installed into
|
|
59
|
+
# `Current.session` itself, which — unlike SessionManager's
|
|
60
|
+
# `eager_load(:account)` path — carries no loaded :account
|
|
61
|
+
# association. Several consumers run `strict_loading_by_default`
|
|
62
|
+
# (jumpdrive-web, luminality-web), where a lazy `record.account`
|
|
63
|
+
# raises StrictLoadingViolationError. Since this emitter is on the
|
|
64
|
+
# path of EVERY authenticated request, that would turn an inert
|
|
65
|
+
# guard into a 500.
|
|
66
|
+
#
|
|
67
|
+
# So: read the association only when it is already loaded, and otherwise
|
|
68
|
+
# fall back to the same single indexed lookup Api::AuthenticationGuard's
|
|
69
|
+
# resolve_account uses. Neither branch can raise under strict loading.
|
|
70
|
+
def resolve_account(browser_session)
|
|
71
|
+
return if browser_session.blank?
|
|
72
|
+
|
|
73
|
+
if browser_session.respond_to?(:association) && browser_session.association(:account).loaded?
|
|
74
|
+
return browser_session.account
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
account_id = browser_session.account_id if browser_session.respond_to?(:account_id)
|
|
78
|
+
return if account_id.blank?
|
|
79
|
+
|
|
80
|
+
StandardId.account_class.find_by(id: account_id)
|
|
81
|
+
end
|
|
50
82
|
end
|
|
51
83
|
end
|
|
52
84
|
end
|