Monorepo for Tangled tangled.org

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
··· 21 21 DBPath string `env:"DB_PATH, default=knotserver.db"` 22 22 Hostname string `env:"HOSTNAME, required"` 23 23 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 24 + Owner string `env:"OWNER, required"` 24 25 LogDids bool `env:"LOG_DIDS, default=true"` 25 26 26 27 // This disables signature verification so use with caution.
+50 -21
knotserver/handler.go
··· 27 27 l *slog.Logger 28 28 n *notifier.Notifier 29 29 resolver *idresolver.Resolver 30 - 31 - // init is a channel that is closed when the knot has been initailized 32 - // i.e. when the first user (knot owner) has been added. 33 - init chan struct{} 34 - knotInitialized bool 35 30 } 36 31 37 32 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 40 jc: jc, 46 41 n: n, 47 42 resolver: idresolver.DefaultResolver(), 48 - init: make(chan struct{}), 49 43 } 50 44 51 45 err := e.AddKnot(rbac.ThisServer) ··· 53 47 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 54 48 } 55 49 50 + err = h.configureOwner() 51 + if err != nil { 52 + return nil, err 53 + } 54 + h.l.Info("owner set", "did", h.c.Server.Owner) 55 + 56 56 err = h.jc.StartJetstream(ctx, h.processMessages) 57 57 if err != nil { 58 58 return nil, fmt.Errorf("failed to start jetstream: %w", err) 59 59 } 60 60 61 - // Check if the knot knows about any Dids; 62 - // if it does, it is already initialized and we can repopulate the 63 - // Jetstream subscriptions. 64 - dids, err := db.GetAllDids() 61 + h.jc.AddDid(h.c.Server.Owner) 62 + 63 + // check if the knot knows about any dids 64 + dids, err := h.db.GetAllDids() 65 65 if err != nil { 66 - return nil, fmt.Errorf("failed to get all Dids: %w", err) 66 + return nil, fmt.Errorf("failed to get all dids: %w", err) 67 67 } 68 - 69 - if len(dids) > 0 { 70 - h.knotInitialized = true 71 - close(h.init) 72 - for _, d := range dids { 73 - h.jc.AddDid(d) 74 - } 68 + for _, d := range dids { 69 + jc.AddDid(d) 75 70 } 76 71 77 72 r.Get("/", h.Index) 78 73 r.Get("/capabilities", h.Capabilities) 79 74 r.Get("/version", h.Version) 75 + r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { 76 + w.Write([]byte(h.c.Server.Owner)) 77 + }) 80 78 r.Route("/{did}", func(r chi.Router) { 81 79 // Repo routes 82 80 r.Route("/{name}", func(r chi.Router) { ··· 154 152 // Socket that streams git oplogs 155 153 r.Get("/events", h.Events) 156 154 157 - // Initialize the knot with an owner and public key. 158 - r.With(h.VerifySignature).Post("/init", h.Init) 159 - 160 155 // Health check. Used for two-way verification with appview. 161 156 r.With(h.VerifySignature).Get("/health", h.Health) 162 157 ··· 211 206 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 212 207 fmt.Fprintf(w, "knotserver/%s", version) 213 208 } 209 + 210 + func (h *Handle) configureOwner() error { 211 + cfgOwner := h.c.Server.Owner 212 + 213 + rbacDomain := "thisserver" 214 + 215 + existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 216 + if err != nil { 217 + return err 218 + } 219 + 220 + switch len(existing) { 221 + case 0: 222 + // no owner configured, continue 223 + case 1: 224 + // find existing owner 225 + existingOwner := existing[0] 226 + 227 + // no ownership change, this is okay 228 + if existingOwner == h.c.Server.Owner { 229 + break 230 + } 231 + 232 + // remove existing owner 233 + err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) 234 + if err != nil { 235 + return nil 236 + } 237 + default: 238 + return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 239 + } 240 + 241 + return h.e.AddKnotOwner(rbacDomain, cfgOwner) 242 + }
-54
knotserver/routes.go
··· 3 3 import ( 4 4 "compress/gzip" 5 5 "context" 6 - "crypto/hmac" 7 6 "crypto/sha256" 8 - "encoding/hex" 9 7 "encoding/json" 10 8 "errors" 11 9 "fmt" ··· 1201 1199 l.Error("setting default branch", "error", err.Error()) 1202 1200 return 1203 1201 } 1204 - 1205 - w.WriteHeader(http.StatusNoContent) 1206 - } 1207 - 1208 - func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { 1209 - l := h.l.With("handler", "Init") 1210 - 1211 - if h.knotInitialized { 1212 - writeError(w, "knot already initialized", http.StatusConflict) 1213 - return 1214 - } 1215 - 1216 - data := struct { 1217 - Did string `json:"did"` 1218 - }{} 1219 - 1220 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1221 - l.Error("failed to decode request body", "error", err.Error()) 1222 - writeError(w, "invalid request body", http.StatusBadRequest) 1223 - return 1224 - } 1225 - 1226 - if data.Did == "" { 1227 - l.Error("empty DID in request", "did", data.Did) 1228 - writeError(w, "did is empty", http.StatusBadRequest) 1229 - return 1230 - } 1231 - 1232 - if err := h.db.AddDid(data.Did); err != nil { 1233 - l.Error("failed to add DID", "error", err.Error()) 1234 - writeError(w, err.Error(), http.StatusInternalServerError) 1235 - return 1236 - } 1237 - h.jc.AddDid(data.Did) 1238 - 1239 - if err := h.e.AddKnotOwner(rbac.ThisServer, data.Did); err != nil { 1240 - l.Error("adding owner", "error", err.Error()) 1241 - writeError(w, err.Error(), http.StatusInternalServerError) 1242 - return 1243 - } 1244 - 1245 - if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1246 - l.Error("fetching and adding keys", "error", err.Error()) 1247 - writeError(w, err.Error(), http.StatusInternalServerError) 1248 - return 1249 - } 1250 - 1251 - close(h.init) 1252 - 1253 - mac := hmac.New(sha256.New, []byte(h.c.Server.Secret)) 1254 - mac.Write([]byte("ok")) 1255 - w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil))) 1256 1202 1257 1203 w.WriteHeader(http.StatusNoContent) 1258 1204 }