Guard repeat comm-prefs setup and cache setup state in memory#10435
Guard repeat comm-prefs setup and cache setup state in memory#10435nopcoder wants to merge 2 commits into
Conversation
SetupCommPrefs now rejects a repeat call with 409 instead of silently overriding stored preferences. Cache the initialized and comm_prefs_set states in memory once observed true so the UI's repeated setup-state polls don't hit KV on every check.
N-o-Z
left a comment
There was a problem hiding this comment.
I can't understand this rationale from a product standpoint.
I would expect users to be able to opt in - in case they initially opted out. This seems like a business interest as well.
I expected this change to check if we're post setup - and if we are to gate this API with an authorization check (e.g.: admin)
SetupCommPrefs now rejects calls before lakeFS is initialized with 412. Setting comm prefs pre-setup let Setup's empty-prefs path overwrite comm_prefs_set with false while the in-memory cache kept true, diverging after restart. Mirrors Setup's own already-initialized guard.
|
@N-o-Z updated the code to check first that init was called before set comm preferences. |
N-o-Z
left a comment
There was a problem hiding this comment.
Thanks!
Reviewed with more comments
| writeError(w, r, http.StatusInternalServerError, err) | ||
| return | ||
| } | ||
| if !initialized { |
There was a problem hiding this comment.
external-RBAC users can never pass the 412 gate, and the API contradicts itself.
With auth.ui_config.rbac=external, Setup returns early before CreateInitialAdminUser, and UpdateSetupTimestamp - the only writer of the setup timestamp anywhere (the cmd/lakefs setup/superuser commands have the same early return) - never runs.
IsInitialized stays false forever, so every SetupCommPrefs returns 412. Meanwhile GetSetupState (controller.go:5488) special-cases external RBAC as initialized - the same server reports "initialized" and "lakeFS is not initialized" simultaneously.
This affects direct API/SDK callers only (the UI never shows the form on external RBAC), but the fix is small: gate on the same condition GetSetupState uses.
| } | ||
| items[SetupAuthTypeKeyPrefix+authType] = setupTimeStr | ||
| return m.writeMetadata(ctx, items) | ||
| err := m.writeMetadata(ctx, items) |
There was a problem hiding this comment.
a partial metadata write now bricks comm prefs permanently.
writeMetadata issues one independent Set per key over random map iteration order, no transaction. If the store errors after comm_prefs_set=true landed but before the email key, the handler returns 500 - and the retry now hits the new 409 (cold cache falls through to KV, reads true), so the email-less record can never be completed. Before this PR a retry simply overwrote and self-healed. Writing comm_prefs_set last as a SetIf commit point fixes this and the once-only guard together.
|
|
||
| // comm prefs may only be set once; do not override existing preferences. | ||
| // ErrNotFound means they were never recorded, so setting is allowed. | ||
| commPrefsSet, err := c.MetadataManager.IsCommPrefsSet(ctx) |
There was a problem hiding this comment.
the once-only guard doesn't prevent overrides under concurrency.
IsCommPrefsSet (read) then UpdateCommPrefs (plain Set, no predicate) is check-then-act. Two concurrent submissions - double-click, two tabs, or two instances sharing one KV - both read "not set", both write, both get 200, and the second silently overwrites the first. That's exactly the override the 409 exists to forbid, plus a duplicate stats event. The repo already has the right primitive: SetIf + kv.ErrPredicateFailed, used by insertOrGetInstallationID (pkg/auth/metadata.go:99). Deeper fix: a MetadataManager.SetCommPrefsOnce doing a conditional put on comm_prefs_set, controller maps predicate failure → 409.
|
|
||
| // cache that comm prefs are set, but only when they actually were: | ||
| // a nil commPrefs writes comm_prefs_set=false and must not prime the cache. | ||
| if commPrefs != nil { |
There was a problem hiding this comment.
the sticky cache diverges from KV via Setup's own retry path (the exact divergence the PR's new comment warns about).
POST /setup with an email calls UpdateCommPrefs(commPrefs) before admin-user creation, priming the cache true. If CreateInitialAdminUser then fails (500 - instance still uninitialized, Setup retryable), a retry of /setup without an email runs UpdateCommPrefs(nil), writing comm_prefs_set=false to KV, but nothing un-sticks the cached true. That process permanently answers "prefs set" (409, comm_prefs_missing=false) while KV, restarts, and other instances say the opposite. Minimal fix: m.commPrefsSet.Store(commPrefs != nil) unconditionally after a successful write.
| return | ||
| } | ||
| if commPrefsSet { | ||
| writeError(w, r, http.StatusConflict, "communication preferences already set") |
There was a problem hiding this comment.
The web UI strands the user on the form when the new 409 fires.
The client maps 409 to throw new Error('Setup is already complete.') (misleading message for this endpoint); in webui/src/pages/setup/index.jsx:56-62 the throw skips setCommPrefsMissing(false) and nothing re-fetches state, so on a double-submit or lost-response retry the user sees an error banner and the form keeps rendering until a manual page reload. Previously the second POST returned 200 and the flow advanced. Minor: treat 409 as success in the submit handler - the prefs are set. (412 falls to "Unknown" but is unreachable through the UI.)
| // comm prefs are part of the post-setup flow; setup must run first. Setting | ||
| // them before setup lets Setup's empty-prefs path later overwrite them with | ||
| // comm_prefs_set=false, diverging from the cached state. | ||
| initialized, err := c.MetadataManager.IsInitialized(ctx) |
There was a problem hiding this comment.
Optimization:
moving email validation here can prevent 2 redundant KV
Summary
SetupCommPrefsnow returns 409 Conflict on a repeat call instead of silently overriding stored communication preferences.initializedandcomm_prefs_setstates in memory (sticky once observed true) so the UI's repeated setup-state polls don't hit KV on every check.