From cfa73fe985bdb5e385331c5387f0c91b1587369a Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 14 Aug 2025 11:19:32 +0300 Subject: [PATCH] knotserver: rework knot ownership process This is now the same as what we do in spindle. Signed-off-by: Anirudh Oppiliappan --- knotserver/config/config.go | 1 + knotserver/handler.go | 71 ++++++++++++++++++++++++++----------- knotserver/routes.go | 54 ---------------------------- 3 files changed, 51 insertions(+), 75 deletions(-) diff --git a/knotserver/config/config.go b/knotserver/config/config.go index a98137b8..5f135ffc 100644 --- a/knotserver/config/config.go +++ b/knotserver/config/config.go @@ -21,6 +21,7 @@ type Server struct { DBPath string `env:"DB_PATH, default=knotserver.db"` Hostname string `env:"HOSTNAME, required"` JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` + Owner string `env:"OWNER, required"` LogDids bool `env:"LOG_DIDS, default=true"` // This disables signature verification so use with caution. diff --git a/knotserver/handler.go b/knotserver/handler.go index 11fc8031..f811c6bf 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -27,11 +27,6 @@ type Handle struct { l *slog.Logger n *notifier.Notifier resolver *idresolver.Resolver - - // init is a channel that is closed when the knot has been initailized - // i.e. when the first user (knot owner) has been added. - init chan struct{} - knotInitialized bool } func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { @@ -45,7 +40,6 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j jc: jc, n: n, resolver: idresolver.DefaultResolver(), - init: make(chan struct{}), } err := e.AddKnot(rbac.ThisServer) @@ -53,30 +47,34 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j return nil, fmt.Errorf("failed to setup enforcer: %w", err) } + err = h.configureOwner() + if err != nil { + return nil, err + } + h.l.Info("owner set", "did", h.c.Server.Owner) + err = h.jc.StartJetstream(ctx, h.processMessages) if err != nil { return nil, fmt.Errorf("failed to start jetstream: %w", err) } - // Check if the knot knows about any Dids; - // if it does, it is already initialized and we can repopulate the - // Jetstream subscriptions. - dids, err := db.GetAllDids() + h.jc.AddDid(h.c.Server.Owner) + + // check if the knot knows about any dids + dids, err := h.db.GetAllDids() if err != nil { - return nil, fmt.Errorf("failed to get all Dids: %w", err) + return nil, fmt.Errorf("failed to get all dids: %w", err) } - - if len(dids) > 0 { - h.knotInitialized = true - close(h.init) - for _, d := range dids { - h.jc.AddDid(d) - } + for _, d := range dids { + jc.AddDid(d) } r.Get("/", h.Index) r.Get("/capabilities", h.Capabilities) r.Get("/version", h.Version) + r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(h.c.Server.Owner)) + }) r.Route("/{did}", func(r chi.Router) { // Repo routes r.Route("/{name}", func(r chi.Router) { @@ -154,9 +152,6 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j // Socket that streams git oplogs r.Get("/events", h.Events) - // Initialize the knot with an owner and public key. - r.With(h.VerifySignature).Post("/init", h.Init) - // Health check. Used for two-way verification with appview. r.With(h.VerifySignature).Get("/health", h.Health) @@ -211,3 +206,37 @@ func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "knotserver/%s", version) } + +func (h *Handle) configureOwner() error { + cfgOwner := h.c.Server.Owner + + rbacDomain := "thisserver" + + existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) + if err != nil { + return err + } + + switch len(existing) { + case 0: + // no owner configured, continue + case 1: + // find existing owner + existingOwner := existing[0] + + // no ownership change, this is okay + if existingOwner == h.c.Server.Owner { + break + } + + // remove existing owner + err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) + if err != nil { + return nil + } + default: + return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) + } + + return h.e.AddKnotOwner(rbacDomain, cfgOwner) +} diff --git a/knotserver/routes.go b/knotserver/routes.go index cda0a570..6eee5445 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -3,9 +3,7 @@ package knotserver import ( "compress/gzip" "context" - "crypto/hmac" "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -1205,58 +1203,6 @@ func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { - l := h.l.With("handler", "Init") - - if h.knotInitialized { - writeError(w, "knot already initialized", http.StatusConflict) - return - } - - data := struct { - Did string `json:"did"` - }{} - - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - l.Error("failed to decode request body", "error", err.Error()) - writeError(w, "invalid request body", http.StatusBadRequest) - return - } - - if data.Did == "" { - l.Error("empty DID in request", "did", data.Did) - writeError(w, "did is empty", http.StatusBadRequest) - return - } - - if err := h.db.AddDid(data.Did); err != nil { - l.Error("failed to add DID", "error", err.Error()) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - h.jc.AddDid(data.Did) - - if err := h.e.AddKnotOwner(rbac.ThisServer, data.Did); err != nil { - l.Error("adding owner", "error", err.Error()) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { - l.Error("fetching and adding keys", "error", err.Error()) - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - close(h.init) - - mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) - mac.Write([]byte("ok")) - w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) - - w.WriteHeader(http.StatusNoContent) -} - func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) } -- 2.51.2