Module: AdminPresence
- Defined in:
- app/lib/admin_presence.rb
Overview
Cross-subdomain "admin presence" cookie that signals "an @warmlyyours.com
admin is currently signed into CRM in this browser." Set by CRM, read by WWW
(and any other sibling app) to decide whether to render admin-only UI like
the cache-purge bar.
Why a separate cookie (and not the Devise session): CRM and WWW have
isolated session cookies on purpose — a customer XSS on www must not bleed
into crm. This cookie carries no auth power of its own; it only signals
presence so the receiving app can mount an admin UI. The receiving app's
privileged endpoints must still re-verify by calling +decode+ themselves.
Properties:
- Encrypted with secret_key_base via +cookies.encrypted+ (tamper-proof)
- HttpOnly + Secure (no JS access; HTTPS only outside test)
- SameSite=Lax (blocks cross-site POSTs; allows top-level navigations)
- Apex domain (.warmlyyours.com) so all subdomains see it
- 1-hour TTL, refreshed on every CRM hit (slides like a session)
SECURITY: Lax cookies are still sent on same-site requests, where
"same-site" = same registrable domain. Any subdomain of warmlyyours.com
can therefore trigger requests with this cookie attached. Endpoints that
act on +decode+ MUST also enforce CSRF protection (do not rely on the
global JSON CSRF bypass), so a sibling-subdomain XSS or takeover cannot
weaponize the cookie. See Www::AdminBarController for the pattern.
Constant Summary collapse
- COOKIE_NAME =
Encrypted, HttpOnly auth carrier. Decoded server-side via +decode+.
:_hw_admin_presence- HINT_COOKIE_NAME =
Plain (NOT HttpOnly) presence flag readable from JS. Lets the client
decide whether to fetch the admin bar without putting bar markup into
the cached HTML. Carries no payload — just "this browser may be admin,
try the fetch." The server still re-validates the encrypted cookie. :_hw_admin_present- TTL =
1.hour
- EMAIL_DOMAIN =
'@warmlyyours.com'- ALLOWED_EMAILS =
Specific @warmlyyours.com employees who get the admin bar even without the
admin role. Keep the list short — for anything broader, give them the role
in CRM. Emails are matched case-insensitively. %w[ jbillen@warmlyyours.com ].map(&:downcase).freeze
Class Method Summary collapse
- .account_eligible?(account) ⇒ Boolean
- .clear!(cookies) ⇒ Object
- .cookie_domain ⇒ Object
-
.decode(cookies) ⇒ Object
Returns the symbolized payload Hash if the cookie is present, decryptable, well-formed, and not expired; otherwise nil.
- .set!(cookies, account) ⇒ Object
-
.sync!(cookies, account) ⇒ Object
Set or clear the cookie based on whether +account+ is an eligible admin.
Class Method Details
.account_eligible?(account) ⇒ Boolean
89 90 91 92 93 94 95 96 97 |
# File 'app/lib/admin_presence.rb', line 89 def account_eligible?(account) return false unless account return false unless account.respond_to?(:email) && account.respond_to?(:is_admin?) email = account.email.to_s.downcase return false unless email.end_with?(EMAIL_DOMAIN) account.is_admin? || ALLOWED_EMAILS.include?(email) end |
.clear!(cookies) ⇒ Object
68 69 70 71 |
# File 'app/lib/admin_presence.rb', line 68 def clear!() .delete(COOKIE_NAME, domain: ) .delete(HINT_COOKIE_NAME, domain: ) end |
.cookie_domain ⇒ Object
99 100 101 |
# File 'app/lib/admin_presence.rb', line 99 def ".#{TLD}" end |
.decode(cookies) ⇒ Object
Returns the symbolized payload Hash if the cookie is present, decryptable,
well-formed, and not expired; otherwise nil. Defense-in-depth: we check
the embedded :exp even though the browser also enforces cookie expiry.
76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'app/lib/admin_presence.rb', line 76 def decode() raw = .encrypted[COOKIE_NAME] return nil unless raw.is_a?(Hash) payload = raw.symbolize_keys return nil unless payload[:exp].is_a?(Integer) && payload[:exp] > Time.current.to_i return nil unless payload[:email].is_a?(String) && payload[:email].downcase.end_with?(EMAIL_DOMAIN) payload rescue StandardError nil end |
.set!(cookies, account) ⇒ Object
57 58 59 60 61 62 63 64 65 66 |
# File 'app/lib/admin_presence.rb', line 57 def set!(, account) expires = TTL.from_now common = { expires: expires, domain: , secure: !Rails.env.test?, same_site: :lax } .encrypted[COOKIE_NAME] = common.merge( value: { aid: account.id, email: account.email, exp: expires.to_i }, httponly: true ) [HINT_COOKIE_NAME] = common.merge(value: '1', httponly: false) end |
.sync!(cookies, account) ⇒ Object
Set or clear the cookie based on whether +account+ is an eligible admin.
Safe to call with nil (clears).
49 50 51 52 53 54 55 |
# File 'app/lib/admin_presence.rb', line 49 def sync!(, account) if account_eligible?(account) set!(, account) else clear!() end end |