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 DBPath string `env:"DB_PATH, default=knotserver.db"` 22 Hostname string `env:"HOSTNAME, required"` 23 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 24 LogDids bool `env:"LOG_DIDS, default=true"` 25 26 // This disables signature verification so use with caution.
··· 21 DBPath string `env:"DB_PATH, default=knotserver.db"` 22 Hostname string `env:"HOSTNAME, required"` 23 JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 24 + Owner string `env:"OWNER, required"` 25 LogDids bool `env:"LOG_DIDS, default=true"` 26 27 // This disables signature verification so use with caution.
+50 -21
knotserver/handler.go
··· 27 l *slog.Logger 28 n *notifier.Notifier 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 } 36 37 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 jc: jc, 46 n: n, 47 resolver: idresolver.DefaultResolver(), 48 - init: make(chan struct{}), 49 } 50 51 err := e.AddKnot(rbac.ThisServer) ··· 53 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 54 } 55 56 err = h.jc.StartJetstream(ctx, h.processMessages) 57 if err != nil { 58 return nil, fmt.Errorf("failed to start jetstream: %w", err) 59 } 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() 65 if err != nil { 66 - return nil, fmt.Errorf("failed to get all Dids: %w", err) 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 - } 75 } 76 77 r.Get("/", h.Index) 78 r.Get("/capabilities", h.Capabilities) 79 r.Get("/version", h.Version) 80 r.Route("/{did}", func(r chi.Router) { 81 // Repo routes 82 r.Route("/{name}", func(r chi.Router) { ··· 154 // Socket that streams git oplogs 155 r.Get("/events", h.Events) 156 157 - // Initialize the knot with an owner and public key. 158 - r.With(h.VerifySignature).Post("/init", h.Init) 159 - 160 // Health check. Used for two-way verification with appview. 161 r.With(h.VerifySignature).Get("/health", h.Health) 162 ··· 211 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 212 fmt.Fprintf(w, "knotserver/%s", version) 213 }
··· 27 l *slog.Logger 28 n *notifier.Notifier 29 resolver *idresolver.Resolver 30 } 31 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) { ··· 40 jc: jc, 41 n: n, 42 resolver: idresolver.DefaultResolver(), 43 } 44 45 err := e.AddKnot(rbac.ThisServer) ··· 47 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 48 } 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 err = h.jc.StartJetstream(ctx, h.processMessages) 57 if err != nil { 58 return nil, fmt.Errorf("failed to start jetstream: %w", err) 59 } 60 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 if err != nil { 66 + return nil, fmt.Errorf("failed to get all dids: %w", err) 67 } 68 + for _, d := range dids { 69 + jc.AddDid(d) 70 } 71 72 r.Get("/", h.Index) 73 r.Get("/capabilities", h.Capabilities) 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 + }) 78 r.Route("/{did}", func(r chi.Router) { 79 // Repo routes 80 r.Route("/{name}", func(r chi.Router) { ··· 152 // Socket that streams git oplogs 153 r.Get("/events", h.Events) 154 155 // Health check. Used for two-way verification with appview. 156 r.With(h.VerifySignature).Get("/health", h.Health) 157 ··· 206 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 207 fmt.Fprintf(w, "knotserver/%s", version) 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 import ( 4 "compress/gzip" 5 "context" 6 - "crypto/hmac" 7 "crypto/sha256" 8 - "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" ··· 1201 l.Error("setting default branch", "error", err.Error()) 1202 return 1203 } 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 1257 w.WriteHeader(http.StatusNoContent) 1258 }
··· 3 import ( 4 "compress/gzip" 5 "context" 6 "crypto/sha256" 7 "encoding/json" 8 "errors" 9 "fmt" ··· 1199 l.Error("setting default branch", "error", err.Error()) 1200 return 1201 } 1202 1203 w.WriteHeader(http.StatusNoContent) 1204 }