forked from tangled.org/core
this repo has no description

knotserver: rework knot ownership process

This is now the same as what we do in spindle.

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi cfa73fe9 4246a57c

verified
Changed files
+51 -75
knotserver
+1
knotserver/config/config.go
···
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.
+50 -21
knotserver/handler.go
···
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) {
···
jc: jc,
n: n,
resolver: idresolver.DefaultResolver(),
-
init: make(chan struct{}),
}
err := e.AddKnot(rbac.ThisServer)
···
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) {
···
// 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)
···
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)
+
}
-54
knotserver/routes.go
···
import (
"compress/gzip"
"context"
-
"crypto/hmac"
"crypto/sha256"
-
"encoding/hex"
"encoding/json"
"errors"
"fmt"
···
l.Error("setting default branch", "error", err.Error())
return
-
-
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)