e9ad719424
0003 built the JetStream KV store (jetstreamStore) but the binary never selected it: membership.Open (SQLite) was hardcoded and OpenJetStream was only reached by migrate-to-kv. This completes the wiring so a node actually serves its control plane from the replicated KV. - New flag --store kv|sqlite (default sqlite). kv opens the JetStream KV control plane over the privileged internal connection; sqlite is the unchanged baseline (branch-by-abstraction: the full suite's SQLite paths are untouched). - Bootstrap cycle resolved with storeHolder: the authenticator consults the holder (fail-closed until set), so it can be built before the KV store exists. The KV store opens after NATS is up and is published into the holder. The only client that can connect in that window is the internal identity, which bypasses the store by key. In SQLite mode the store is set before StartServer, so the window does not exist. - needJS now covers --store kv as well as --cluster-name; the JetStream client is shared by the KV store and the replicated nonce bucket. - feature_flags.json: decentralized wiring documented as complete, realized via --store kv (opt-in per deploy; default stays sqlite). Fail-closed preserved: jetstreamStore.IsAuthorized already denies on any backend error; the holder denies while unset. Tests: - TestStoreHolderFailClosed: empty holder denies; serves after set. - TestKVStoreBootstrapUnderEnforce: end-to-end decentralized boot — KV-seeded user authenticates over nkey under enforce; outsider denied. - TestKVStoreDecentralizedConsistency: a room/user created on one node's KV store is visible to another's (ends the per-node SQLite divergence, audit 0008 N5). CGO_ENABLED=0 go build/vet/test green; govulncheck 0 reachable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
1.9 KiB
Go
61 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/enmanuel/unibus/pkg/membership"
|
|
)
|
|
|
|
// storeHolder is a concurrency-safe slot for the control-plane store, used to
|
|
// break the decentralized bootstrap cycle (issue 0006c): the NATS authenticator
|
|
// must be built BEFORE the embedded server starts, but the JetStream KV store can
|
|
// only be opened AFTER NATS is up (it needs a JetStream client). The authenticator
|
|
// therefore consults the holder instead of a concrete store.
|
|
//
|
|
// Fail-closed by construction: until the store is set, IsAuthorized denies and
|
|
// SubjectACL errors, so any client connecting in the startup window is rejected.
|
|
// The only connection expected in that window is membershipd's own internal
|
|
// service identity, which the authenticator recognizes by key and lets through
|
|
// without consulting the store at all. In the SQLite (default) path the store is
|
|
// set before StartServer, so the window does not exist and behavior is identical
|
|
// to the pre-0006c baseline.
|
|
type storeHolder struct {
|
|
mu sync.RWMutex
|
|
s membership.Store
|
|
}
|
|
|
|
func (h *storeHolder) set(s membership.Store) {
|
|
h.mu.Lock()
|
|
h.s = s
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *storeHolder) get() membership.Store {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.s
|
|
}
|
|
|
|
// IsAuthorized reports whether signPubHex is an active bus user, denying while the
|
|
// store is not yet set (fail closed). It is the predicate the nkey authenticator
|
|
// uses for every connecting client.
|
|
func (h *storeHolder) IsAuthorized(signPubHex string) bool {
|
|
s := h.get()
|
|
if s == nil {
|
|
return false
|
|
}
|
|
return s.IsAuthorized(signPubHex)
|
|
}
|
|
|
|
// subjectACL derives the per-subject permissions for signPubHex via the live
|
|
// store, erroring (so the caller fails closed and denies the connection) while the
|
|
// store is not yet set.
|
|
func (h *storeHolder) subjectACL(signPubHex string) ([]string, error) {
|
|
s := h.get()
|
|
if s == nil {
|
|
return nil, fmt.Errorf("control-plane store not ready")
|
|
}
|
|
return membership.SubjectACLFor(s)(signPubHex)
|
|
}
|