Repository navigation
feat(databases): warn when a stored JWT is expired or expiring - #46036
gabotorresruiz wants to merge 7 commits into
Conversation
Add a helper that reports, for every declared sensitive path of a database's encrypted_extra, the length of the stored string and its last few characters, so a masked secret can be told apart from another one (for example after a token rotation). The number of characters comes from the new ENCRYPTED_EXTRA_HINT_CHARS setting (0 disables, which is the default) and is exposed to the frontend. At most 25% of a trimmed value is ever revealed: values shorter than four times the configured length only report their length. Non-string values are skipped, wildcard paths expand per match, and output is sorted by path. BaseEngineSpec.encrypted_extra_hints builds on encrypted_extra_sensitive_field_paths and takes labels from encrypted_extra_sensitive_fields when it is a dict. A value that is a single PEM block (a service account or key-pair private key) always ends with the same "-----END ...-----" footer, so for those the suffix and the 25% cap use the base64 body with line breaks and newline escapes removed. The reported length stays the length of the stored value.
Add unhappy-path tests: unicode and trailing-newline values, a stored value equal to the mask literal, non-object or malformed JSON, and a 1 MB secret next to thousands of other keys.
…ra fields Add GET /api/v1/database/<pk>/encrypted_extra_hints/, which returns the label, path, length and short suffix of each sensitive field stored in a database's encrypted_extra, so an editor can tell which secret is stored without it being revealed. The suffix length comes only from the ENCRYPTED_EXTRA_HINT_CHARS config; 0 or less returns an empty list. The route maps to can_write on Database, so only users who can edit a connection can call it. Calls go to the event log with the database id and never the hint values, and the response is dumped through the schema so nothing beyond the four declared keys can leave the server. The published OpenAPI spec gains the new path and its two schemas.
…formation Add the JSONPath and label of sensitive encrypted_extra fields to the engine information returned by the database endpoints. The connection modal needs to know, before any click, which Secure extra fields are stored secrets and what to call them, so it can render a field per stored secret up front and let the reveal control only change the value. The database GET, the list and the available endpoint carry the engine's declared patterns. The connection endpoint, which the edit modal loads and which requires write access to databases, replaces them with the fields stored for that database: every string value at a sensitive path, keyed by its concrete path. An engine on the catch-all "$.*" rule therefore shows one entry per stored string key instead of the pattern, and objects are left out. The label is the declared one when the path is declared exactly, else the key name. A missing or malformed encrypted_extra gives an empty map. Readers of a database see nothing about what is stored. BaseEngineSpec gains encrypted_extra_sensitive_field_labels(), which maps each path to its declared label (or the last path segment) with keys sorted. The OAuth2 client secret that every engine adds is labeled "OAuth2 client secret" unless the engine declares its own label. encrypted_extra_hints reads its labels from the same method, and a new get_sensitive_field_paths helper shares its path resolution with the hints, so both list the same fields. Database.stored_sensitive_fields() builds the per-database map. The published OpenAPI spec documents the new key.
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #46036 +/- ##
==========================================
+ Coverage 84.96% 85.01% +0.04%
==========================================
Files 3078 3082 +4
Lines 208652 209376 +724
Branches 49167 49286 +119
==========================================
+ Hits 177288 178004 +716
+ Misses 27992 27989 -3
- Partials 3372 3383 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| const left = Date.parse(expiresAt) - Date.now(); | ||
| if (Number.isNaN(left) || left > EXPIRY_WARNING_DAYS * DAY_MS) { | ||
| return undefined; |
There was a problem hiding this comment.
Suggestion: When the browser clock lags the server by over seven days, this filters out a server-unexpired token that expires soon, hiding its warning.
Assessment: 🟠 Major · 🔁 Occurrence: Rarely · 🏷️ Logic error
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset-frontend/src/features/databases/DatabaseModal/StoredSecretExpiryAlert.tsx
**Line:** 45:47
**Comment:**
*Logic Error: When the browser clock lags the server by over seven days, this filters out a server-unexpired token that expires soon, hiding its warning.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if isinstance(exp, bool) or not isinstance(exp, int): | ||
| return None, None |
There was a problem hiding this comment.
Suggestion: Valid JWT NumericDate claims may contain fractional seconds, but this rejects every float, so those tokens never get an expiry or near-expiry warning.
Assessment: 🟠 Major · 🔁 Occurrence: Rarely · 🏷️ Type error
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset/utils/json.py
**Line:** 413:414
**Comment:**
*Type Error: Valid JWT NumericDate claims may contain fractional seconds, but this rejects every float, so those tokens never get an expiry or near-expiry warning.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThere was a problem hiding this comment.
✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 3c26786.
The exp claim now accepts both integers and floats, rejects non-finite floats, and passes fractional timestamps to datetime.fromtimestamp, allowing fractional-second JWT expirations.
If that's not right, unresolve this thread and CodeAnt will leave it open.
e9dbd02 to
9e8be08
Compare
Database editors cannot tell which secret is saved in Secure extra, because the modal only shows the masked value. When a token is rotated or pasted into the wrong connection, the only way to check was to replace the secret blindly. Render one read-only field per stored secret under Secure extra. The fields are known before any request: the modal matches the masked values of Secure extra against the sensitive fields listed in the engine information of the connection payload, so each field shows its final label and its storage path from the start. A concrete path keeps its own label when a wildcard also matches it. Like a password field, each field has its own eye, and it changes only that field's value. The first eye clicked sends one request to the encrypted_extra_hints endpoint and shows a loading state; an eye clicked while that request is pending waits on it and loads too. The answer is kept, so later eyes reveal their fields without a second request. A revealed field shows masked bullets followed by the last characters of its secret, and its eye hides it again without touching the other fields. A failed request warns under the field whose eye sent it and can be retried from any eye. The label, the helper text and the input itself stay the same across a reveal. A secret too short to reveal says so, with a tooltip giving the minimum length, which is four times ENCRYPTED_EXTRA_HINT_CHARS. Nothing is fetched until an eye is clicked, so opening the modal never exposes even a partial secret. The fields render only when editing a saved database whose Secure extra still holds a masked value at a declared path and the ENCRYPTED_EXTRA_HINT_CHARS config is above zero. The hints live in local state: a hidden field drops its ending from the page, and closing the modal discards them.
Revealing the ending of a stored secret tells an editor which token is stored. It does not tell them whether that token still works, and an expired JWT fails only when a query runs. The reveal now also says when a stored JWT expires, read from its exp claim. Backend: `jwt_expiry` in `superset.utils.json` decodes the payload segment of a three-segment base64url value whose header segment decodes to a JSON object, and reads only `exp`: any finite number in range, as RFC 7519 allows a non-integer one. The signature is not verified (the server has no key; the claim is informational) and no other claim is read, returned or logged. Anything else, a PEM key or a plain password for example, gives nulls. It never raises. Each hint from `encrypted_extra_hints` gains `expires_at` (UTC) and `expired`, computed against the server clock, and both are null for values that are not such a JWT. The response schema, the OpenAPI spec and the engine spec README describe the two keys. Frontend: in the revealed field's slot, `Expires <date>` in secondary text or `Expired <date>` in the danger colour, with the date in the app's locale-aware short format and a tooltip saying it is read from the exp claim. A field without an expiry shows nothing there, and the too-short and not-available notes keep the slot when they apply. The three new strings are added to messages.pot; the weekly translation sync workflow propagates the template into the catalogs.
A database whose stored JWT had expired kept failing every query until someone found the cause. The edit modal can say so as soon as it opens, before any click and without a new request: the connection payload it already loads now carries when each stored token expires. - Database.stored_secret_expirations() maps each stored sensitive field holding a JWT with an exp claim to its expiry and whether it has passed, read from the unverified exp claim. Other secrets, such as PEM keys and passwords, are left out. - GET /api/v1/database/<pk>/connection sets it as engine_information.encrypted_extra_expirations, next to the stored sensitive fields. The plain GET, the list and available do not carry it, so readers learn nothing new and pay nothing. - EngineInformationSchema documents the map; the OpenAPI spec gains the EncryptedExtraExpiration component. - StoredSecretExpiryAlert shows one dismissible warning at the top of the edit modal, above the Basic and Advanced tabs, listing each stored secret that has expired or expires within 7 calendar days, by its label: "The stored JWT Token expired on <date>. Paste a new token into Secure extra and save.", "The stored JWT Token expires in 5 days (<date>)." or, on its last day, "The stored JWT Token expires today (<date>)." Days are counted between calendar dates in the browser's time zone, the same dates the line shows. Nothing shows in create mode or when no entry qualifies. Whether a token has expired is decided on the server. - The warning does not depend on ENCRYPTED_EXTRA_HINT_CHARS: it carries no secret material, only when a token expires, and it is the part that prevents an outage, so an install with the reveal off still gets it. - The three new strings are in messages.pot; the weekly translation sync workflow propagates the template into the catalogs.
9e8be08 to
3c26786
Compare
SUMMARY
An expired JWT in a connection's Secure extra fails every query until someone opens the connection and notices. This PR warns the editor the moment the database modal opens, before any tab, panel or reveal click, when a stored JWT is expired or expires within seven days.
Stacked on #46035 (itself stacked on #45997); the first six commits belong to those PRs and this diff shrinks to the last commit once they merge.
What changes:
Database.stored_secret_expirations()returns, for each stored sensitive field whose value is a JWT withexp, its expiry and whether it is past, using the same decoder as the hints (payload only,exponly, never raises).GET /api/v1/database/<pk>/connection, the write-gated payload the edit form loads, carries it asengine_information.encrypted_extra_expirations. The plain database GET, the list andavailabledo not carry it, and tests pin that, so readers see nothing new and SQL Lab bootstrap does no extra work.StoredSecretExpiryAlertcomponent renders once above the tabs of the modal in edit mode, so it is visible from both Basic and Advanced, using the same wrapper and alert styling as the modal's other alerts. One dismissible warning lists each affected secret by its label:The stored JWT Token expired on Nov 4, 2026. Paste a new token into Secure extra and save.,The stored JWT Token expires in 5 days (Nov 10, 2026).orThe stored JWT Token expires today (Nov 5, 2026).Days are counted on the calendar, so the number always agrees with the date beside it. Nothing renders in create mode, without the map, or when no secret is expired or within seven days.Compatibility: no config change. The warning does not depend on
ENCRYPTED_EXTRA_HINT_CHARS, because it carries no secret material (an expiry date, not characters of the token) and it is the part that prevents the outage, so an installation with the reveal switched off still gets it. An older frontend ignores the new key.BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
Expired token, first open of the modal, nothing expanded:
Token expiring in 5 days:
Token 60 days out, no warning:
Create mode, no warning:
TESTING INSTRUCTIONS
Automated:
tests/unit_tests/models/core_test.py: expirations for an expiring and an expired token with a PEM key alongside (only the two JWTs listed), six empty or malformed cases, only declared fields considered.tests/unit_tests/databases/api_test.py:/connectioncarries the map; the plain GET, the list andavailabledo not.DatabaseModal/index.test.tsx: expired and 7-day tokens warn as soon as the modal renders in edit mode without opening any tab or panel, several secrets in one alert, no warning at 8 days, in create mode, or without the map;StoredSecretExpiryAlert.test.tsxcovers the component and the path fallback for an unlabelled field.Manual:
expis yesterday in a Druid connection's Secure extra, open Edit: the warning shows above the tabs with no hints request in the network tab; dismiss it; reopen: it is back.ENCRYPTED_EXTRA_HINT_CHARS = 0: the warning still shows, the reveal field does not.ADDITIONAL INFORMATION