Skip to content

Guard repeat comm-prefs setup and cache setup state in memory#10435

Open
nopcoder wants to merge 2 commits into
masterfrom
chore/setup-comm
Open

Guard repeat comm-prefs setup and cache setup state in memory#10435
nopcoder wants to merge 2 commits into
masterfrom
chore/setup-comm

Conversation

@nopcoder

@nopcoder nopcoder commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • SetupCommPrefs now returns 409 Conflict on a repeat call instead of silently overriding stored communication preferences.
  • Cache the initialized and comm_prefs_set states in memory (sticky once observed true) so the UI's repeated setup-state polls don't hit KV on every check.
  • These states only ever transition false→true, so a cached true is correct indefinitely.

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.
@nopcoder nopcoder added exclude-changelog PR description should not be included in next release changelog minor-change Used for PRs that don't require issue attached mostly-ai labels Jun 8, 2026
@nopcoder nopcoder self-assigned this Jun 8, 2026
@github-actions github-actions Bot added area/testing Improvements or additions to tests area/auth IAM, authorization, authentication, audit, AAA, and integrations with all those labels Jun 8, 2026
@nopcoder
nopcoder requested a review from a team June 8, 2026 11:15

@N-o-Z N-o-Z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@nopcoder

Copy link
Copy Markdown
Contributor Author

@N-o-Z updated the code to check first that init was called before set comm preferences.
also cached the results as they are one way on success. This should prevent database access after the setup process after first check.

@nopcoder
nopcoder requested a review from N-o-Z June 25, 2026 14:47

@N-o-Z N-o-Z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!
Reviewed with more comments

Comment thread pkg/api/controller.go
writeError(w, r, http.StatusInternalServerError, err)
return
}
if !initialized {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/auth/metadata.go
}
items[SetupAuthTypeKeyPrefix+authType] = setupTimeStr
return m.writeMetadata(ctx, items)
err := m.writeMetadata(ctx, items)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing

Comment thread pkg/api/controller.go

// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/auth/metadata.go

// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/api/controller.go
return
}
if commPrefsSet {
writeError(w, r, http.StatusConflict, "communication preferences already set")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment thread pkg/api/controller.go
// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization:
moving email validation here can prevent 2 redundant KV

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth IAM, authorization, authentication, audit, AAA, and integrations with all those area/testing Improvements or additions to tests exclude-changelog PR description should not be included in next release changelog minor-change Used for PRs that don't require issue attached mostly-ai

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants