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

appview: implement new ownership process for knots

This is now identical to how we verify spindle registrations, and gets
rid of the registration key. This code is now deduplicated in the
serververify package (previously spindleverify).

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

authored by anirudh.fi and committed by oppi.li 3d9fc547 395156e1

Changed files
+603 -349
appview
knots
serververify
spindles
spindleverify
state
rbac
+3 -3
appview/ingester.go
··· 14 14 "tangled.sh/tangled.sh/core/api/tangled" 15 15 "tangled.sh/tangled.sh/core/appview/config" 16 16 "tangled.sh/tangled.sh/core/appview/db" 17 - "tangled.sh/tangled.sh/core/appview/spindleverify" 17 + "tangled.sh/tangled.sh/core/appview/serververify" 18 18 "tangled.sh/tangled.sh/core/idresolver" 19 19 "tangled.sh/tangled.sh/core/rbac" 20 20 ) ··· 475 475 return err 476 476 } 477 477 478 - err = spindleverify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 478 + err = serververify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 479 479 if err != nil { 480 480 l.Error("failed to add spindle to db", "err", err, "instance", instance) 481 481 return err 482 482 } 483 483 484 - _, err = spindleverify.MarkVerified(ddb, i.Enforcer, instance, did) 484 + _, err = serververify.MarkSpindleVerified(ddb, i.Enforcer, instance, did) 485 485 if err != nil { 486 486 return fmt.Errorf("failed to mark verified: %w", err) 487 487 }
+420 -217
appview/knots/knots.go
··· 1 1 package knots 2 2 3 3 import ( 4 - "context" 5 - "crypto/hmac" 6 - "crypto/sha256" 7 - "encoding/hex" 4 + "errors" 8 5 "fmt" 9 6 "log/slog" 10 7 "net/http" 11 - "strings" 8 + "slices" 12 9 "time" 13 10 14 11 "github.com/go-chi/chi/v5" ··· 18 15 "tangled.sh/tangled.sh/core/appview/middleware" 19 16 "tangled.sh/tangled.sh/core/appview/oauth" 20 17 "tangled.sh/tangled.sh/core/appview/pages" 18 + "tangled.sh/tangled.sh/core/appview/serververify" 21 19 "tangled.sh/tangled.sh/core/eventconsumer" 22 20 "tangled.sh/tangled.sh/core/idresolver" 23 - "tangled.sh/tangled.sh/core/knotclient" 24 21 "tangled.sh/tangled.sh/core/rbac" 25 22 "tangled.sh/tangled.sh/core/tid" 26 23 ··· 39 36 Knotstream *eventconsumer.Consumer 40 37 } 41 38 42 - func (k *Knots) Router(mw *middleware.Middleware) http.Handler { 39 + func (k *Knots) Router() http.Handler { 43 40 r := chi.NewRouter() 44 41 45 - r.Use(middleware.AuthMiddleware(k.OAuth)) 42 + r.With(middleware.AuthMiddleware(k.OAuth)).Get("/", k.knots) 43 + r.With(middleware.AuthMiddleware(k.OAuth)).Post("/register", k.register) 46 44 47 - r.Get("/", k.index) 48 - r.Post("/key", k.generateKey) 45 + r.With(middleware.AuthMiddleware(k.OAuth)).Get("/{domain}", k.dashboard) 46 + r.With(middleware.AuthMiddleware(k.OAuth)).Delete("/{domain}", k.delete) 49 47 50 - r.Route("/{domain}", func(r chi.Router) { 51 - r.Post("/init", k.init) 52 - r.Get("/", k.dashboard) 53 - r.Route("/member", func(r chi.Router) { 54 - r.Use(mw.KnotOwner()) 55 - r.Get("/", k.members) 56 - r.Put("/", k.addMember) 57 - r.Delete("/", k.removeMember) 58 - }) 59 - }) 48 + r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry) 49 + r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember) 50 + r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember) 60 51 61 52 return r 62 53 } 63 54 64 - // get knots registered by this user 65 - func (k *Knots) index(w http.ResponseWriter, r *http.Request) { 66 - l := k.Logger.With("handler", "index") 67 - 55 + func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 68 56 user := k.OAuth.GetUser(r) 69 57 registrations, err := db.RegistrationsByDid(k.Db, user.Did) 70 58 if err != nil { 71 - l.Error("failed to get registrations by did", "err", err) 59 + k.Logger.Error("failed to fetch knot registrations", "err", err) 60 + w.WriteHeader(http.StatusInternalServerError) 61 + return 72 62 } 73 63 74 64 k.Pages.Knots(w, pages.KnotsParams{ ··· 77 67 }) 78 68 } 79 69 80 - // requires auth 81 - func (k *Knots) generateKey(w http.ResponseWriter, r *http.Request) { 82 - l := k.Logger.With("handler", "generateKey") 70 + func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 71 + l := k.Logger.With("handler", "dashboard") 83 72 84 73 user := k.OAuth.GetUser(r) 85 - did := user.Did 86 - l = l.With("did", did) 74 + l = l.With("user", user.Did) 87 75 88 - // check if domain is valid url, and strip extra bits down to just host 89 - domain := r.FormValue("domain") 76 + domain := chi.URLParam(r, "domain") 90 77 if domain == "" { 91 - l.Error("empty domain") 92 - http.Error(w, "Invalid form", http.StatusBadRequest) 93 78 return 94 79 } 95 80 l = l.With("domain", domain) 96 81 97 - noticeId := "registration-error" 98 - fail := func() { 99 - k.Pages.Notice(w, noticeId, "Failed to generate registration key.") 82 + registrations, err := db.GetRegistrations( 83 + k.Db, 84 + db.FilterEq("did", user.Did), 85 + db.FilterEq("domain", domain), 86 + ) 87 + if err != nil { 88 + l.Error("failed to get registrations", "err", err) 89 + http.Error(w, "Not found", http.StatusNotFound) 90 + return 100 91 } 101 92 102 - key, err := db.GenerateRegistrationKey(k.Db, domain, did) 93 + // Find the specific registration for this domain 94 + var registration *db.Registration 95 + for _, reg := range registrations { 96 + if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil { 97 + registration = &reg 98 + break 99 + } 100 + } 101 + 102 + if registration == nil { 103 + l.Error("registration not found or not verified") 104 + http.Error(w, "Not found", http.StatusNotFound) 105 + return 106 + } 107 + registration := registrations[0] 108 + 109 + members, err := k.Enforcer.GetUserByRole("server:member", domain) 103 110 if err != nil { 104 - l.Error("failed to generate registration key", "err", err) 105 - fail() 111 + l.Error("failed to get knot members", "err", err) 112 + http.Error(w, "Not found", http.StatusInternalServerError) 106 113 return 107 114 } 115 + slices.Sort(members) 108 116 109 - allRegs, err := db.RegistrationsByDid(k.Db, did) 117 + repos, err := db.GetRepos( 118 + k.Db, 119 + 0, 120 + db.FilterEq("knot", domain), 121 + ) 110 122 if err != nil { 111 - l.Error("failed to generate registration key", "err", err) 112 - fail() 123 + l.Error("failed to get knot repos", "err", err) 124 + http.Error(w, "Not found", http.StatusInternalServerError) 113 125 return 114 126 } 115 127 116 - k.Pages.KnotListingFull(w, pages.KnotListingFullParams{ 117 - Registrations: allRegs, 118 - }) 119 - k.Pages.KnotSecret(w, pages.KnotSecretParams{ 120 - Secret: key, 128 + // organize repos by did 129 + repoMap := make(map[string][]db.Repo) 130 + for _, r := range repos { 131 + repoMap[r.Did] = append(repoMap[r.Did], r) 132 + } 133 + 134 + k.Pages.Knot(w, pages.KnotParams{ 135 + LoggedInUser: user, 136 + Registration: &registration, 137 + Members: members, 138 + Repos: repoMap, 139 + IsOwner: true, 121 140 }) 122 141 } 123 142 124 - // create a signed request and check if a node responds to that 125 - func (k *Knots) init(w http.ResponseWriter, r *http.Request) { 126 - l := k.Logger.With("handler", "init") 143 + func (k *Knots) register(w http.ResponseWriter, r *http.Request) { 127 144 user := k.OAuth.GetUser(r) 145 + l := k.Logger.With("handler", "register") 128 146 129 - noticeId := "operation-error" 130 - defaultErr := "Failed to initialize knot. Try again later." 147 + noticeId := "register-error" 148 + defaultErr := "Failed to register knot. Try again later." 131 149 fail := func() { 132 150 k.Pages.Notice(w, noticeId, defaultErr) 133 151 } 134 152 135 - domain := chi.URLParam(r, "domain") 153 + domain := r.FormValue("domain") 136 154 if domain == "" { 137 - http.Error(w, "malformed url", http.StatusBadRequest) 155 + k.Pages.Notice(w, noticeId, "Incomplete form.") 138 156 return 139 157 } 140 158 l = l.With("domain", domain) 159 + l = l.With("user", user.Did) 141 160 142 - l.Info("checking domain") 161 + tx, err := k.Db.Begin() 162 + if err != nil { 163 + l.Error("failed to start transaction", "err", err) 164 + fail() 165 + return 166 + } 167 + defer func() { 168 + tx.Rollback() 169 + k.Enforcer.E.LoadPolicy() 170 + }() 143 171 144 - registration, err := db.RegistrationByDomain(k.Db, domain) 172 + err = db.AddKnot(tx, domain, user.Did) 145 173 if err != nil { 146 - l.Error("failed to get registration for domain", "err", err) 174 + l.Error("failed to insert", "err", err) 147 175 fail() 148 176 return 149 177 } 150 - if registration.ByDid != user.Did { 151 - l.Error("unauthorized", "wantedDid", registration.ByDid, "gotDid", user.Did) 152 - w.WriteHeader(http.StatusUnauthorized) 178 + 179 + err = k.Enforcer.AddKnot(domain) 180 + if err != nil { 181 + l.Error("failed to create knot", "err", err) 182 + fail() 153 183 return 154 184 } 155 185 156 - secret, err := db.GetRegistrationKey(k.Db, domain) 186 + // create record on pds 187 + client, err := k.OAuth.AuthorizedClient(r) 157 188 if err != nil { 158 - l.Error("failed to get registration key for domain", "err", err) 189 + l.Error("failed to authorize client", "err", err) 159 190 fail() 160 191 return 161 192 } 162 193 163 - client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 194 + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 195 + var exCid *string 196 + if ex != nil { 197 + exCid = ex.Cid 198 + } 199 + 200 + // re-announce by registering under same rkey 201 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 202 + Collection: tangled.KnotNSID, 203 + Repo: user.Did, 204 + Rkey: domain, 205 + Record: &lexutil.LexiconTypeDecoder{ 206 + Val: &tangled.Knot{ 207 + CreatedAt: time.Now().Format(time.RFC3339), 208 + }, 209 + }, 210 + SwapRecord: exCid, 211 + }) 212 + 164 213 if err != nil { 165 - l.Error("failed to create knotclient", "err", err) 214 + l.Error("failed to put record", "err", err) 166 215 fail() 167 216 return 168 217 } 169 218 170 - resp, err := client.Init(user.Did) 219 + err = tx.Commit() 171 220 if err != nil { 172 - k.Pages.Notice(w, noticeId, fmt.Sprintf("Failed to make request: %s", err.Error())) 173 - l.Error("failed to make init request", "err", err) 221 + l.Error("failed to commit transaction", "err", err) 222 + fail() 174 223 return 175 224 } 176 225 177 - if resp.StatusCode == http.StatusConflict { 178 - k.Pages.Notice(w, noticeId, "This knot is already registered") 179 - l.Error("knot already registered", "statuscode", resp.StatusCode) 226 + err = k.Enforcer.E.SavePolicy() 227 + if err != nil { 228 + l.Error("failed to update ACL", "err", err) 229 + k.Pages.HxRefresh(w) 180 230 return 181 231 } 182 232 183 - if resp.StatusCode != http.StatusNoContent { 184 - k.Pages.Notice(w, noticeId, fmt.Sprintf("Received status %d from knot, expected %d", resp.StatusCode, http.StatusNoContent)) 185 - l.Error("incorrect statuscode returned", "statuscode", resp.StatusCode, "expected", http.StatusNoContent) 233 + // begin verification 234 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 235 + if err != nil { 236 + l.Error("verification failed", "err", err) 237 + k.Pages.HxRefresh(w) 186 238 return 187 239 } 188 240 189 - // verify response mac 190 - signature := resp.Header.Get("X-Signature") 191 - signatureBytes, err := hex.DecodeString(signature) 241 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 192 242 if err != nil { 243 + l.Error("failed to mark verified", "err", err) 244 + k.Pages.HxRefresh(w) 193 245 return 194 246 } 195 247 196 - expectedMac := hmac.New(sha256.New, []byte(secret)) 197 - expectedMac.Write([]byte("ok")) 248 + // add this knot to knotstream 249 + go k.Knotstream.AddSource( 250 + r.Context(), 251 + eventconsumer.NewKnotSource(domain), 252 + ) 198 253 199 - if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 200 - k.Pages.Notice(w, noticeId, "Response signature mismatch, consider regenerating the secret and retrying.") 201 - l.Error("signature mismatch", "bytes", signatureBytes) 254 + // ok 255 + k.Pages.HxRefresh(w) 256 + } 257 + 258 + func (k *Knots) delete(w http.ResponseWriter, r *http.Request) { 259 + user := k.OAuth.GetUser(r) 260 + l := k.Logger.With("handler", "delete") 261 + 262 + noticeId := "operation-error" 263 + defaultErr := "Failed to delete knot. Try again later." 264 + fail := func() { 265 + k.Pages.Notice(w, noticeId, defaultErr) 266 + } 267 + 268 + domain := chi.URLParam(r, "domain") 269 + if domain == "" { 270 + l.Error("empty domain") 271 + fail() 272 + return 273 + } 274 + 275 + // get record from db first 276 + registrations, err := db.GetRegistrations( 277 + k.Db, 278 + db.FilterEq("did", user.Did), 279 + db.FilterEq("domain", domain), 280 + ) 281 + if err != nil { 282 + l.Error("failed to get registration", "err", err) 283 + fail() 202 284 return 203 285 } 286 + if len(registrations) != 1 { 287 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 288 + fail() 289 + return 290 + } 291 + registration := registrations[0] 204 292 205 - tx, err := k.Db.BeginTx(r.Context(), nil) 293 + tx, err := k.Db.Begin() 206 294 if err != nil { 207 - l.Error("failed to start tx", "err", err) 295 + l.Error("failed to start txn", "err", err) 208 296 fail() 209 297 return 210 298 } 211 299 defer func() { 212 300 tx.Rollback() 213 - err = k.Enforcer.E.LoadPolicy() 214 - if err != nil { 215 - l.Error("rollback failed", "err", err) 216 - } 301 + k.Enforcer.E.LoadPolicy() 217 302 }() 218 303 219 - // mark as registered 220 - err = db.Register(tx, domain) 304 + err = db.DeleteKnot( 305 + tx, 306 + db.FilterEq("did", user.Did), 307 + db.FilterEq("domain", domain), 308 + ) 221 309 if err != nil { 222 - l.Error("failed to register domain", "err", err) 310 + l.Error("failed to delete registration", "err", err) 223 311 fail() 224 312 return 225 313 } 226 314 227 - // set permissions for this did as owner 228 - reg, err := db.RegistrationByDomain(tx, domain) 229 - if err != nil { 230 - l.Error("failed get registration by domain", "err", err) 231 - fail() 232 - return 315 + // delete from enforcer if it was registered 316 + if registration.Registered != nil { 317 + err = k.Enforcer.RemoveKnot(domain) 318 + if err != nil { 319 + l.Error("failed to update ACL", "err", err) 320 + fail() 321 + return 322 + } 233 323 } 234 324 235 - // add basic acls for this domain 236 - err = k.Enforcer.AddKnot(domain) 325 + client, err := k.OAuth.AuthorizedClient(r) 237 326 if err != nil { 238 - l.Error("failed to add knot to enforcer", "err", err) 327 + l.Error("failed to authorize client", "err", err) 239 328 fail() 240 329 return 241 330 } 242 331 243 - // add this did as owner of this domain 244 - err = k.Enforcer.AddKnotOwner(domain, reg.ByDid) 332 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 333 + Collection: tangled.KnotNSID, 334 + Repo: user.Did, 335 + Rkey: domain, 336 + }) 245 337 if err != nil { 246 - l.Error("failed to add knot owner to enforcer", "err", err) 247 - fail() 248 - return 338 + // non-fatal 339 + l.Error("failed to delete record", "err", err) 249 340 } 250 341 251 342 err = tx.Commit() 252 343 if err != nil { 253 - l.Error("failed to commit changes", "err", err) 344 + l.Error("failed to delete knot", "err", err) 254 345 fail() 255 346 return 256 347 } 257 348 258 349 err = k.Enforcer.E.SavePolicy() 259 350 if err != nil { 260 - l.Error("failed to update ACLs", "err", err) 261 - fail() 351 + l.Error("failed to update ACL", "err", err) 352 + k.Pages.HxRefresh(w) 262 353 return 263 354 } 264 355 265 - // add this knot to knotstream 266 - go k.Knotstream.AddSource( 267 - context.Background(), 268 - eventconsumer.NewKnotSource(domain), 269 - ) 356 + shouldRedirect := r.Header.Get("shouldRedirect") 357 + if shouldRedirect == "true" { 358 + k.Pages.HxRedirect(w, "/knots") 359 + return 360 + } 270 361 271 - k.Pages.KnotListing(w, pages.KnotListingParams{ 272 - Registration: *reg, 273 - }) 362 + w.Write([]byte{}) 274 363 } 275 364 276 - func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 277 - l := k.Logger.With("handler", "dashboard") 365 + func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { 366 + user := k.OAuth.GetUser(r) 367 + l := k.Logger.With("handler", "retry") 368 + 369 + noticeId := "operation-error" 370 + defaultErr := "Failed to verify knot. Try again later." 278 371 fail := func() { 279 - w.WriteHeader(http.StatusInternalServerError) 372 + k.Pages.Notice(w, noticeId, defaultErr) 280 373 } 281 374 282 375 domain := chi.URLParam(r, "domain") 283 376 if domain == "" { 284 - http.Error(w, "malformed url", http.StatusBadRequest) 377 + l.Error("empty domain") 378 + fail() 285 379 return 286 380 } 287 381 l = l.With("domain", domain) 382 + l = l.With("user", user.Did) 288 383 289 - user := k.OAuth.GetUser(r) 290 - l = l.With("did", user.Did) 291 - 292 - // dashboard is only available to owners 293 - ok, err := k.Enforcer.IsKnotOwner(user.Did, domain) 384 + // get record from db first 385 + registrations, err := db.GetRegistrations( 386 + k.Db, 387 + db.FilterEq("did", user.Did), 388 + db.FilterEq("domain", domain), 389 + ) 294 390 if err != nil { 295 - l.Error("failed to query enforcer", "err", err) 391 + l.Error("failed to get registration", "err", err) 296 392 fail() 393 + return 297 394 } 298 - if !ok { 299 - http.Error(w, "only owners can view dashboards", http.StatusUnauthorized) 395 + if len(registrations) != 1 { 396 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 397 + fail() 300 398 return 301 399 } 400 + registration := registrations[0] 302 401 303 - reg, err := db.RegistrationByDomain(k.Db, domain) 402 + // begin verification 403 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 304 404 if err != nil { 305 - l.Error("failed to get registration by domain", "err", err) 405 + l.Error("verification failed", "err", err) 406 + 407 + if errors.Is(err, serververify.FetchError) { 408 + k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") 409 + return 410 + } 411 + 412 + if e, ok := err.(*serververify.OwnerMismatch); ok { 413 + k.Pages.Notice(w, noticeId, e.Error()) 414 + return 415 + } 416 + 306 417 fail() 307 418 return 308 419 } 309 420 310 - var members []string 311 - if reg.Registered != nil { 312 - members, err = k.Enforcer.GetUserByRole("server:member", domain) 421 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 422 + if err != nil { 423 + l.Error("failed to mark verified", "err", err) 424 + k.Pages.Notice(w, noticeId, err.Error()) 425 + return 426 + } 427 + 428 + // if this knot was previously read-only, then emit a record too 429 + // 430 + // this is part of migrating from the old knot system to the new one 431 + if registration.ReadOnly { 432 + // re-announce by registering under same rkey 433 + client, err := k.OAuth.AuthorizedClient(r) 313 434 if err != nil { 314 - l.Error("failed to get members list", "err", err) 435 + l.Error("failed to authorize client", "err", err) 315 436 fail() 316 437 return 317 438 } 439 + 440 + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 441 + var exCid *string 442 + if ex != nil { 443 + exCid = ex.Cid 444 + } 445 + 446 + // ignore the error here 447 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 448 + Collection: tangled.KnotNSID, 449 + Repo: user.Did, 450 + Rkey: domain, 451 + Record: &lexutil.LexiconTypeDecoder{ 452 + Val: &tangled.Knot{ 453 + CreatedAt: time.Now().Format(time.RFC3339), 454 + }, 455 + }, 456 + SwapRecord: exCid, 457 + }) 458 + if err != nil { 459 + l.Error("non-fatal: failed to reannouce knot", "err", err) 460 + } 318 461 } 319 462 320 - repos, err := db.GetRepos( 463 + // add this knot to knotstream 464 + go k.Knotstream.AddSource( 465 + r.Context(), 466 + eventconsumer.NewKnotSource(domain), 467 + ) 468 + 469 + shouldRefresh := r.Header.Get("shouldRefresh") 470 + if shouldRefresh == "true" { 471 + k.Pages.HxRefresh(w) 472 + return 473 + } 474 + 475 + // Get updated registration to show 476 + registrations, err = db.GetRegistrations( 321 477 k.Db, 322 - 0, 323 - db.FilterEq("knot", domain), 324 - db.FilterIn("did", members), 478 + db.FilterEq("did", user.Did), 479 + db.FilterEq("domain", domain), 325 480 ) 326 481 if err != nil { 327 - l.Error("failed to get repos list", "err", err) 482 + l.Error("failed to get registration", "err", err) 328 483 fail() 329 484 return 330 485 } 331 - // convert to map 332 - repoByMember := make(map[string][]db.Repo) 333 - for _, r := range repos { 334 - repoByMember[r.Did] = append(repoByMember[r.Did], r) 335 - } 336 - 337 - k.Pages.Knot(w, pages.KnotParams{ 338 - LoggedInUser: user, 339 - Registration: reg, 340 - Members: members, 341 - Repos: repoByMember, 342 - IsOwner: true, 343 - }) 344 - } 345 - 346 - // list members of domain, requires auth and requires owner status 347 - func (k *Knots) members(w http.ResponseWriter, r *http.Request) { 348 - l := k.Logger.With("handler", "members") 349 - 350 - domain := chi.URLParam(r, "domain") 351 - if domain == "" { 352 - http.Error(w, "malformed url", http.StatusBadRequest) 486 + if len(registrations) != 1 { 487 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 488 + fail() 353 489 return 354 490 } 355 - l = l.With("domain", domain) 491 + updatedRegistration := registrations[0] 356 492 357 - // list all members for this domain 358 - memberDids, err := k.Enforcer.GetUserByRole("server:member", domain) 359 - if err != nil { 360 - w.Write([]byte("failed to fetch member list")) 361 - return 362 - } 493 + log.Println(updatedRegistration) 363 494 364 - w.Write([]byte(strings.Join(memberDids, "\n"))) 495 + w.Header().Set("HX-Reswap", "outerHTML") 496 + k.Pages.KnotListing(w, pages.KnotListingParams{ 497 + Registration: &updatedRegistration, 498 + }) 365 499 } 366 500 367 - // add member to domain, requires auth and requires invite access 368 501 func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 369 - l := k.Logger.With("handler", "members") 502 + user := k.OAuth.GetUser(r) 503 + l := k.Logger.With("handler", "addMember") 370 504 371 505 domain := chi.URLParam(r, "domain") 372 506 if domain == "" { 373 - http.Error(w, "malformed url", http.StatusBadRequest) 507 + l.Error("empty domain") 508 + http.Error(w, "Not found", http.StatusNotFound) 374 509 return 375 510 } 376 511 l = l.With("domain", domain) 512 + l = l.With("user", user.Did) 377 513 378 - reg, err := db.RegistrationByDomain(k.Db, domain) 514 + registrations, err := db.GetRegistrations( 515 + k.Db, 516 + db.FilterEq("did", user.Did), 517 + db.FilterEq("domain", domain), 518 + db.FilterIsNot("registered", "null"), 519 + ) 379 520 if err != nil { 380 - l.Error("failed to get registration by domain", "err", err) 381 - http.Error(w, "malformed url", http.StatusBadRequest) 521 + l.Error("failed to retrieve domain registration", "err", err) 522 + http.Error(w, "Not found", http.StatusNotFound) 382 523 return 383 524 } 384 525 385 - noticeId := fmt.Sprintf("add-member-error-%d", reg.Id) 386 - l = l.With("notice-id", noticeId) 526 + noticeId := fmt.Sprintf("add-member-error-%d", registration.Id) 387 527 defaultErr := "Failed to add member. Try again later." 388 528 fail := func() { 389 529 k.Pages.Notice(w, noticeId, defaultErr) 390 530 } 391 531 392 - subjectIdentifier := r.FormValue("subject") 393 - if subjectIdentifier == "" { 394 - http.Error(w, "malformed form", http.StatusBadRequest) 532 + member := r.FormValue("member") 533 + if member == "" { 534 + l.Error("empty member") 535 + k.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 395 536 return 396 537 } 397 - l = l.With("subjectIdentifier", subjectIdentifier) 538 + l = l.With("member", member) 398 539 399 - subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier) 540 + memberId, err := k.IdResolver.ResolveIdent(r.Context(), member) 400 541 if err != nil { 401 - l.Error("failed to resolve identity", "err", err) 542 + l.Error("failed to resolve member identity to handle", "err", err) 402 543 k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 403 544 return 404 545 } 405 - l = l.With("subjectDid", subjectIdentity.DID) 406 - 407 - l.Info("adding member to knot") 546 + if memberId.Handle.IsInvalidHandle() { 547 + l.Error("failed to resolve member identity to handle") 548 + k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 549 + return 550 + } 408 551 409 - // announce this relation into the firehose, store into owners' pds 552 + // write to pds 410 553 client, err := k.OAuth.AuthorizedClient(r) 411 554 if err != nil { 412 - l.Error("failed to create client", "err", err) 555 + l.Error("failed to authorize client", "err", err) 413 556 fail() 414 557 return 415 558 } 416 559 417 - currentUser := k.OAuth.GetUser(r) 418 - createdAt := time.Now().Format(time.RFC3339) 419 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 560 + rkey := tid.TID() 561 + 562 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 420 563 Collection: tangled.KnotMemberNSID, 421 - Repo: currentUser.Did, 422 - Rkey: tid.TID(), 564 + Repo: user.Did, 565 + Rkey: rkey, 423 566 Record: &lexutil.LexiconTypeDecoder{ 424 567 Val: &tangled.KnotMember{ 425 - Subject: subjectIdentity.DID.String(), 568 + CreatedAt: time.Now().Format(time.RFC3339), 426 569 Domain: domain, 427 - CreatedAt: createdAt, 428 - }}, 570 + Subject: memberId.DID.String(), 571 + }, 572 + }, 429 573 }) 430 - // invalid record 431 574 if err != nil { 432 - l.Error("failed to write to PDS", "err", err) 575 + l.Error("failed to add record to PDS", "err", err) 576 + k.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") 577 + return 578 + } 579 + 580 + err = k.Enforcer.AddKnotMember(domain, memberId.DID.String()) 581 + if err != nil { 582 + l.Error("failed to add member to ACLs", "err", err) 433 583 fail() 434 584 return 435 585 } 436 - l = l.With("at-uri", resp.Uri) 437 - l.Info("wrote record to PDS") 438 586 439 - secret, err := db.GetRegistrationKey(k.Db, domain) 587 + err = k.Enforcer.E.SavePolicy() 440 588 if err != nil { 441 - l.Error("failed to get registration key", "err", err) 589 + l.Error("failed to save ACL policy", "err", err) 590 + fail() 591 + return 592 + } 593 + 594 + // success 595 + k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) 596 + } 597 + 598 + func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 599 + user := k.OAuth.GetUser(r) 600 + l := k.Logger.With("handler", "removeMember") 601 + 602 + noticeId := "operation-error" 603 + defaultErr := "Failed to remove member. Try again later." 604 + fail := func() { 605 + k.Pages.Notice(w, noticeId, defaultErr) 606 + } 607 + 608 + domain := chi.URLParam(r, "domain") 609 + if domain == "" { 610 + l.Error("empty domain") 442 611 fail() 443 612 return 444 613 } 614 + l = l.With("domain", domain) 615 + l = l.With("user", user.Did) 445 616 446 - ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 617 + registrations, err := db.GetRegistrations( 618 + k.Db, 619 + db.FilterEq("did", user.Did), 620 + db.FilterEq("domain", domain), 621 + db.FilterIsNot("registered", "null"), 622 + ) 447 623 if err != nil { 448 - l.Error("failed to create client", "err", err) 449 - fail() 624 + l.Error("failed to get registration", "err", err) 625 + return 626 + } 627 + if len(registrations) != 1 { 628 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 450 629 return 451 630 } 452 631 453 - ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) 632 + member := r.FormValue("member") 633 + if member == "" { 634 + l.Error("empty member") 635 + k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.") 636 + return 637 + } 638 + l = l.With("member", member) 639 + 640 + memberId, err := k.IdResolver.ResolveIdent(r.Context(), member) 454 641 if err != nil { 455 - l.Error("failed to reach knotserver", "err", err) 456 - k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.") 642 + l.Error("failed to resolve member identity to handle", "err", err) 643 + k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 644 + return 645 + } 646 + if memberId.Handle.IsInvalidHandle() { 647 + l.Error("failed to resolve member identity to handle") 648 + k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 457 649 return 458 650 } 459 651 460 - if ksResp.StatusCode != http.StatusNoContent { 461 - l.Error("status mismatch", "got", ksResp.StatusCode, "expected", http.StatusNoContent) 462 - k.Pages.Notice(w, noticeId, fmt.Sprintf("Unexpected status code from knotserver %d, expected %d", ksResp.StatusCode, http.StatusNoContent)) 652 + // remove from enforcer 653 + err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String()) 654 + if err != nil { 655 + l.Error("failed to update ACLs", "err", err) 656 + fail() 463 657 return 464 658 } 465 659 466 - err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) 660 + client, err := k.OAuth.AuthorizedClient(r) 467 661 if err != nil { 468 - l.Error("failed to add member to enforcer", "err", err) 662 + l.Error("failed to authorize client", "err", err) 469 663 fail() 470 664 return 471 665 } 472 666 473 - // success 474 - k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) 475 - } 667 + // TODO: We need to track the rkey for knot members to delete the record 668 + // For now, just remove from ACLs 669 + _ = client 670 + 671 + // commit everything 672 + err = k.Enforcer.E.SavePolicy() 673 + if err != nil { 674 + l.Error("failed to save ACLs", "err", err) 675 + fail() 676 + return 677 + } 476 678 477 - func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 679 + // ok 680 + k.Pages.HxRefresh(w) 478 681 }
+164
appview/serververify/verify.go
··· 1 + package serververify 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "strings" 10 + "time" 11 + 12 + "tangled.sh/tangled.sh/core/appview/db" 13 + "tangled.sh/tangled.sh/core/rbac" 14 + ) 15 + 16 + var ( 17 + FetchError = errors.New("failed to fetch owner") 18 + ) 19 + 20 + // fetchOwner fetches the owner DID from a server's /owner endpoint 21 + func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 22 + scheme := "https" 23 + if dev { 24 + scheme = "http" 25 + } 26 + 27 + url := fmt.Sprintf("%s://%s/owner", scheme, domain) 28 + req, err := http.NewRequest("GET", url, nil) 29 + if err != nil { 30 + return "", err 31 + } 32 + 33 + client := &http.Client{ 34 + Timeout: 1 * time.Second, 35 + } 36 + 37 + resp, err := client.Do(req.WithContext(ctx)) 38 + if err != nil || resp.StatusCode != 200 { 39 + return "", fmt.Errorf("failed to fetch /owner") 40 + } 41 + 42 + body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 43 + if err != nil { 44 + return "", fmt.Errorf("failed to read /owner response: %w", err) 45 + } 46 + 47 + did := strings.TrimSpace(string(body)) 48 + if did == "" { 49 + return "", fmt.Errorf("empty DID in /owner response") 50 + } 51 + 52 + return did, nil 53 + } 54 + 55 + type OwnerMismatch struct { 56 + expected string 57 + observed string 58 + } 59 + 60 + func (e *OwnerMismatch) Error() string { 61 + return fmt.Sprintf("owner mismatch: %q != %q", e.expected, e.observed) 62 + } 63 + 64 + // RunVerification verifies that the server at the given domain has the expected owner 65 + func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool) error { 66 + observedOwner, err := fetchOwner(ctx, domain, dev) 67 + if err != nil { 68 + return fmt.Errorf("%w: %w", FetchError, err) 69 + } 70 + 71 + if observedOwner != expectedOwner { 72 + return &OwnerMismatch{ 73 + expected: expectedOwner, 74 + observed: observedOwner, 75 + } 76 + } 77 + 78 + return nil 79 + } 80 + 81 + // MarkSpindleVerified marks a spindle as verified in the DB and adds the user as its owner 82 + func MarkSpindleVerified(d *db.DB, e *rbac.Enforcer, instance, owner string) (int64, error) { 83 + tx, err := d.Begin() 84 + if err != nil { 85 + return 0, fmt.Errorf("failed to create txn: %w", err) 86 + } 87 + defer func() { 88 + tx.Rollback() 89 + e.E.LoadPolicy() 90 + }() 91 + 92 + // mark this spindle as verified in the db 93 + rowId, err := db.VerifySpindle( 94 + tx, 95 + db.FilterEq("owner", owner), 96 + db.FilterEq("instance", instance), 97 + ) 98 + if err != nil { 99 + return 0, fmt.Errorf("failed to write to DB: %w", err) 100 + } 101 + 102 + err = e.AddSpindleOwner(instance, owner) 103 + if err != nil { 104 + return 0, fmt.Errorf("failed to update ACL: %w", err) 105 + } 106 + 107 + err = tx.Commit() 108 + if err != nil { 109 + return 0, fmt.Errorf("failed to commit txn: %w", err) 110 + } 111 + 112 + err = e.E.SavePolicy() 113 + if err != nil { 114 + return 0, fmt.Errorf("failed to update ACL: %w", err) 115 + } 116 + 117 + return rowId, nil 118 + } 119 + 120 + // MarkKnotVerified marks a knot as verified and sets up ownership/permissions 121 + func MarkKnotVerified(d *db.DB, e *rbac.Enforcer, domain, owner string) error { 122 + tx, err := d.BeginTx(context.Background(), nil) 123 + if err != nil { 124 + return fmt.Errorf("failed to start tx: %w", err) 125 + } 126 + defer func() { 127 + tx.Rollback() 128 + e.E.LoadPolicy() 129 + }() 130 + 131 + // mark as registered 132 + err = db.MarkRegistered( 133 + tx, 134 + db.FilterEq("did", owner), 135 + db.FilterEq("domain", domain), 136 + ) 137 + if err != nil { 138 + return fmt.Errorf("failed to register domain: %w", err) 139 + } 140 + 141 + // add basic acls for this domain 142 + err = e.AddKnot(domain) 143 + if err != nil { 144 + return fmt.Errorf("failed to add knot to enforcer: %w", err) 145 + } 146 + 147 + // add this did as owner of this domain 148 + err = e.AddKnotOwner(domain, owner) 149 + if err != nil { 150 + return fmt.Errorf("failed to add knot owner to enforcer: %w", err) 151 + } 152 + 153 + err = tx.Commit() 154 + if err != nil { 155 + return fmt.Errorf("failed to commit changes: %w", err) 156 + } 157 + 158 + err = e.E.SavePolicy() 159 + if err != nil { 160 + return fmt.Errorf("failed to update ACLs: %w", err) 161 + } 162 + 163 + return nil 164 + }
+8 -8
appview/spindles/spindles.go
··· 15 15 "tangled.sh/tangled.sh/core/appview/middleware" 16 16 "tangled.sh/tangled.sh/core/appview/oauth" 17 17 "tangled.sh/tangled.sh/core/appview/pages" 18 - verify "tangled.sh/tangled.sh/core/appview/spindleverify" 18 + "tangled.sh/tangled.sh/core/appview/serververify" 19 19 "tangled.sh/tangled.sh/core/idresolver" 20 20 "tangled.sh/tangled.sh/core/rbac" 21 21 "tangled.sh/tangled.sh/core/tid" ··· 227 227 } 228 228 229 229 // begin verification 230 - err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 230 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 231 231 if err != nil { 232 232 l.Error("verification failed", "err", err) 233 233 s.Pages.HxRefresh(w) 234 234 return 235 235 } 236 236 237 - _, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 237 + _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 238 238 if err != nil { 239 239 l.Error("failed to mark verified", "err", err) 240 240 s.Pages.HxRefresh(w) ··· 400 400 } 401 401 402 402 // begin verification 403 - err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 403 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 404 404 if err != nil { 405 405 l.Error("verification failed", "err", err) 406 406 407 - if errors.Is(err, verify.FetchError) { 408 - s.Pages.Notice(w, noticeId, err.Error()) 407 + if errors.Is(err, serververify.FetchError) { 408 + s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") 409 409 return 410 410 } 411 411 412 - if e, ok := err.(*verify.OwnerMismatch); ok { 412 + if e, ok := err.(*serververify.OwnerMismatch); ok { 413 413 s.Pages.Notice(w, noticeId, e.Error()) 414 414 return 415 415 } ··· 418 418 return 419 419 } 420 420 421 - rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 421 + rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 422 422 if err != nil { 423 423 l.Error("failed to mark verified", "err", err) 424 424 s.Pages.Notice(w, noticeId, err.Error())
-118
appview/spindleverify/verify.go
··· 1 - package spindleverify 2 - 3 - import ( 4 - "context" 5 - "errors" 6 - "fmt" 7 - "io" 8 - "net/http" 9 - "strings" 10 - "time" 11 - 12 - "tangled.sh/tangled.sh/core/appview/db" 13 - "tangled.sh/tangled.sh/core/rbac" 14 - ) 15 - 16 - var ( 17 - FetchError = errors.New("failed to fetch owner") 18 - ) 19 - 20 - // TODO: move this to "spindleclient" or similar 21 - func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 22 - scheme := "https" 23 - if dev { 24 - scheme = "http" 25 - } 26 - 27 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 28 - req, err := http.NewRequest("GET", url, nil) 29 - if err != nil { 30 - return "", err 31 - } 32 - 33 - client := &http.Client{ 34 - Timeout: 1 * time.Second, 35 - } 36 - 37 - resp, err := client.Do(req.WithContext(ctx)) 38 - if err != nil || resp.StatusCode != 200 { 39 - return "", fmt.Errorf("failed to fetch /owner") 40 - } 41 - 42 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 43 - if err != nil { 44 - return "", fmt.Errorf("failed to read /owner response: %w", err) 45 - } 46 - 47 - did := strings.TrimSpace(string(body)) 48 - if did == "" { 49 - return "", fmt.Errorf("empty DID in /owner response") 50 - } 51 - 52 - return did, nil 53 - } 54 - 55 - type OwnerMismatch struct { 56 - expected string 57 - observed string 58 - } 59 - 60 - func (e *OwnerMismatch) Error() string { 61 - return fmt.Sprintf("owner mismatch: %q != %q", e.expected, e.observed) 62 - } 63 - 64 - func RunVerification(ctx context.Context, instance, expectedOwner string, dev bool) error { 65 - // begin verification 66 - observedOwner, err := fetchOwner(ctx, instance, dev) 67 - if err != nil { 68 - return fmt.Errorf("%w: %w", FetchError, err) 69 - } 70 - 71 - if observedOwner != expectedOwner { 72 - return &OwnerMismatch{ 73 - expected: expectedOwner, 74 - observed: observedOwner, 75 - } 76 - } 77 - 78 - return nil 79 - } 80 - 81 - // mark this spindle as verified in the DB and add this user as its owner 82 - func MarkVerified(d *db.DB, e *rbac.Enforcer, instance, owner string) (int64, error) { 83 - tx, err := d.Begin() 84 - if err != nil { 85 - return 0, fmt.Errorf("failed to create txn: %w", err) 86 - } 87 - defer func() { 88 - tx.Rollback() 89 - e.E.LoadPolicy() 90 - }() 91 - 92 - // mark this spindle as verified in the db 93 - rowId, err := db.VerifySpindle( 94 - tx, 95 - db.FilterEq("owner", owner), 96 - db.FilterEq("instance", instance), 97 - ) 98 - if err != nil { 99 - return 0, fmt.Errorf("failed to write to DB: %w", err) 100 - } 101 - 102 - err = e.AddSpindleOwner(instance, owner) 103 - if err != nil { 104 - return 0, fmt.Errorf("failed to update ACL: %w", err) 105 - } 106 - 107 - err = tx.Commit() 108 - if err != nil { 109 - return 0, fmt.Errorf("failed to commit txn: %w", err) 110 - } 111 - 112 - err = e.E.SavePolicy() 113 - if err != nil { 114 - return 0, fmt.Errorf("failed to update ACL: %w", err) 115 - } 116 - 117 - return rowId, nil 118 - }
+3 -3
appview/state/router.go
··· 147 147 148 148 r.Mount("/settings", s.SettingsRouter()) 149 149 r.Mount("/strings", s.StringsRouter(mw)) 150 - r.Mount("/knots", s.KnotsRouter(mw)) 150 + r.Mount("/knots", s.KnotsRouter()) 151 151 r.Mount("/spindles", s.SpindlesRouter()) 152 152 r.Mount("/signup", s.SignupRouter()) 153 153 r.Mount("/", s.OAuthRouter()) ··· 195 195 return spindles.Router() 196 196 } 197 197 198 - func (s *State) KnotsRouter(mw *middleware.Middleware) http.Handler { 198 + func (s *State) KnotsRouter() http.Handler { 199 199 logger := log.New("knots") 200 200 201 201 knots := &knots.Knots{ ··· 209 209 Logger: logger, 210 210 } 211 211 212 - return knots.Router(mw) 212 + return knots.Router() 213 213 } 214 214 215 215 func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler {
+5
rbac/rbac.go
··· 100 100 return err 101 101 } 102 102 103 + func (e *Enforcer) RemoveKnot(knot string) error { 104 + _, err := e.E.DeleteDomains(knot) 105 + return err 106 + } 107 + 103 108 func (e *Enforcer) GetKnotsForUser(did string) ([]string, error) { 104 109 keepFunc := isNotSpindle 105 110 stripFunc := unSpindle