Monorepo for Tangled tangled.org

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>

anirudh.fi 9db30f0a eb4ce023

verified
Changed files
+542 -363
appview
knots
serververify
spindles
spindleverify
state
+371 -234
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.RegistrationsByDid(k.Db, user.Did) 83 + if err != nil { 84 + l.Error("failed to get registrations", "err", err) 85 + http.Error(w, "Not found", http.StatusNotFound) 86 + return 100 87 } 101 88 102 - key, err := db.GenerateRegistrationKey(k.Db, domain, did) 89 + // Find the specific registration for this domain 90 + var registration *db.Registration 91 + for _, reg := range registrations { 92 + if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil { 93 + registration = &reg 94 + break 95 + } 96 + } 97 + 98 + if registration == nil { 99 + l.Error("registration not found or not verified") 100 + http.Error(w, "Not found", http.StatusNotFound) 101 + return 102 + } 103 + 104 + members, err := k.Enforcer.GetUserByRole("server:member", domain) 103 105 if err != nil { 104 - l.Error("failed to generate registration key", "err", err) 105 - fail() 106 + l.Error("failed to get knot members", "err", err) 107 + http.Error(w, "Not found", http.StatusInternalServerError) 106 108 return 107 109 } 110 + slices.Sort(members) 108 111 109 - allRegs, err := db.RegistrationsByDid(k.Db, did) 112 + repos, err := db.GetRepos( 113 + k.Db, 114 + 0, 115 + db.FilterEq("knot", domain), 116 + ) 110 117 if err != nil { 111 - l.Error("failed to generate registration key", "err", err) 112 - fail() 118 + l.Error("failed to get knot repos", "err", err) 119 + http.Error(w, "Not found", http.StatusInternalServerError) 113 120 return 114 121 } 115 122 116 - k.Pages.KnotListingFull(w, pages.KnotListingFullParams{ 117 - Registrations: allRegs, 118 - }) 119 - k.Pages.KnotSecret(w, pages.KnotSecretParams{ 120 - Secret: key, 123 + identsToResolve := make([]string, len(members)) 124 + copy(identsToResolve, members) 125 + resolvedIds := k.IdResolver.ResolveIdents(r.Context(), identsToResolve) 126 + didHandleMap := make(map[string]string) 127 + for _, identity := range resolvedIds { 128 + if !identity.Handle.IsInvalidHandle() { 129 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 130 + } else { 131 + didHandleMap[identity.DID.String()] = identity.DID.String() 132 + } 133 + } 134 + 135 + // organize repos by did 136 + repoMap := make(map[string][]db.Repo) 137 + for _, r := range repos { 138 + repoMap[r.Did] = append(repoMap[r.Did], r) 139 + } 140 + 141 + k.Pages.Knot(w, pages.KnotParams{ 142 + LoggedInUser: user, 143 + Registration: registration, 144 + Members: members, 145 + Repos: repoMap, 146 + DidHandleMap: didHandleMap, 147 + IsOwner: true, 121 148 }) 122 149 } 123 150 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") 151 + func (k *Knots) register(w http.ResponseWriter, r *http.Request) { 127 152 user := k.OAuth.GetUser(r) 153 + l := k.Logger.With("handler", "register") 128 154 129 - noticeId := "operation-error" 130 - defaultErr := "Failed to initialize knot. Try again later." 155 + noticeId := "register-error" 156 + defaultErr := "Failed to register knot. Try again later." 131 157 fail := func() { 132 158 k.Pages.Notice(w, noticeId, defaultErr) 133 159 } 134 160 135 - domain := chi.URLParam(r, "domain") 161 + domain := r.FormValue("domain") 136 162 if domain == "" { 137 - http.Error(w, "malformed url", http.StatusBadRequest) 163 + k.Pages.Notice(w, noticeId, "Incomplete form.") 138 164 return 139 165 } 140 166 l = l.With("domain", domain) 167 + l = l.With("user", user.Did) 141 168 142 - l.Info("checking domain") 169 + tx, err := k.Db.Begin() 170 + if err != nil { 171 + l.Error("failed to start transaction", "err", err) 172 + fail() 173 + return 174 + } 175 + defer func() { 176 + tx.Rollback() 177 + k.Enforcer.E.LoadPolicy() 178 + }() 143 179 144 - registration, err := db.RegistrationByDomain(k.Db, domain) 180 + err = db.AddKnot(tx, domain, user.Did) 145 181 if err != nil { 146 - l.Error("failed to get registration for domain", "err", err) 182 + l.Error("failed to insert", "err", err) 147 183 fail() 148 184 return 149 185 } 150 - if registration.ByDid != user.Did { 151 - l.Error("unauthorized", "wantedDid", registration.ByDid, "gotDid", user.Did) 152 - w.WriteHeader(http.StatusUnauthorized) 186 + 187 + err = k.Enforcer.AddKnot(domain) 188 + if err != nil { 189 + l.Error("failed to create knot", "err", err) 190 + fail() 153 191 return 154 192 } 155 193 156 - secret, err := db.GetRegistrationKey(k.Db, domain) 194 + // create record on pds 195 + client, err := k.OAuth.AuthorizedClient(r) 157 196 if err != nil { 158 - l.Error("failed to get registration key for domain", "err", err) 197 + l.Error("failed to authorize client", "err", err) 159 198 fail() 160 199 return 161 200 } 162 201 163 - client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 202 + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 203 + var exCid *string 204 + if ex != nil { 205 + exCid = ex.Cid 206 + } 207 + 208 + // re-announce by registering under same rkey 209 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 210 + Collection: tangled.KnotNSID, 211 + Repo: user.Did, 212 + Rkey: domain, 213 + Record: &lexutil.LexiconTypeDecoder{ 214 + Val: &tangled.Knot{ 215 + CreatedAt: time.Now().Format(time.RFC3339), 216 + }, 217 + }, 218 + SwapRecord: exCid, 219 + }) 220 + 164 221 if err != nil { 165 - l.Error("failed to create knotclient", "err", err) 222 + l.Error("failed to put record", "err", err) 166 223 fail() 167 224 return 168 225 } 169 226 170 - resp, err := client.Init(user.Did) 227 + err = tx.Commit() 171 228 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) 229 + l.Error("failed to commit transaction", "err", err) 230 + fail() 174 231 return 175 232 } 176 233 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) 234 + err = k.Enforcer.E.SavePolicy() 235 + if err != nil { 236 + l.Error("failed to update ACL", "err", err) 237 + k.Pages.HxRefresh(w) 180 238 return 181 239 } 182 240 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) 241 + // begin verification 242 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 243 + if err != nil { 244 + l.Error("verification failed", "err", err) 245 + k.Pages.HxRefresh(w) 186 246 return 187 247 } 188 248 189 - // verify response mac 190 - signature := resp.Header.Get("X-Signature") 191 - signatureBytes, err := hex.DecodeString(signature) 249 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 192 250 if err != nil { 251 + l.Error("failed to mark verified", "err", err) 252 + k.Pages.HxRefresh(w) 193 253 return 194 254 } 195 255 196 - expectedMac := hmac.New(sha256.New, []byte(secret)) 197 - expectedMac.Write([]byte("ok")) 256 + // add this knot to knotstream 257 + go k.Knotstream.AddSource( 258 + r.Context(), 259 + eventconsumer.NewKnotSource(domain), 260 + ) 261 + 262 + // ok 263 + k.Pages.HxRefresh(w) 264 + } 265 + 266 + func (k *Knots) delete(w http.ResponseWriter, r *http.Request) { 267 + user := k.OAuth.GetUser(r) 268 + l := k.Logger.With("handler", "delete") 269 + 270 + noticeId := "operation-error" 271 + defaultErr := "Failed to delete knot. Try again later." 272 + fail := func() { 273 + k.Pages.Notice(w, noticeId, defaultErr) 274 + } 198 275 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) 276 + domain := chi.URLParam(r, "domain") 277 + if domain == "" { 278 + l.Error("empty domain") 279 + fail() 202 280 return 203 281 } 204 282 205 - tx, err := k.Db.BeginTx(r.Context(), nil) 283 + registration, err := db.RegistrationByDomain(k.Db, domain) 206 284 if err != nil { 207 - l.Error("failed to start tx", "err", err) 285 + l.Error("failed to retrieve domain registration", "err", err) 286 + fail() 287 + return 288 + } 289 + 290 + if registration.ByDid != user.Did { 291 + l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 292 + k.Pages.Notice(w, noticeId, "Failed to delete knot, unauthorized deletion attempt.") 293 + return 294 + } 295 + 296 + tx, err := k.Db.Begin() 297 + if err != nil { 298 + l.Error("failed to start txn", "err", err) 208 299 fail() 209 300 return 210 301 } 211 302 defer func() { 212 303 tx.Rollback() 213 - err = k.Enforcer.E.LoadPolicy() 214 - if err != nil { 215 - l.Error("rollback failed", "err", err) 216 - } 304 + k.Enforcer.E.LoadPolicy() 217 305 }() 218 306 219 - // mark as registered 220 - err = db.Register(tx, domain) 307 + err = db.DeleteKnot( 308 + tx, 309 + db.FilterEq("did", user.Did), 310 + db.FilterEq("domain", domain), 311 + ) 221 312 if err != nil { 222 - l.Error("failed to register domain", "err", err) 313 + l.Error("failed to delete registration", "err", err) 223 314 fail() 224 315 return 225 316 } 226 317 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 318 + // delete from enforcer if it was registered 319 + if registration.Registered != nil { 320 + err = k.Enforcer.RemoveKnot(domain) 321 + if err != nil { 322 + l.Error("failed to update ACL", "err", err) 323 + fail() 324 + return 325 + } 233 326 } 234 327 235 - // add basic acls for this domain 236 - err = k.Enforcer.AddKnot(domain) 328 + client, err := k.OAuth.AuthorizedClient(r) 237 329 if err != nil { 238 - l.Error("failed to add knot to enforcer", "err", err) 330 + l.Error("failed to authorize client", "err", err) 239 331 fail() 240 332 return 241 333 } 242 334 243 - // add this did as owner of this domain 244 - err = k.Enforcer.AddKnotOwner(domain, reg.ByDid) 335 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 336 + Collection: tangled.KnotNSID, 337 + Repo: user.Did, 338 + Rkey: domain, 339 + }) 245 340 if err != nil { 246 - l.Error("failed to add knot owner to enforcer", "err", err) 247 - fail() 248 - return 341 + // non-fatal 342 + l.Error("failed to delete record", "err", err) 249 343 } 250 344 251 345 err = tx.Commit() 252 346 if err != nil { 253 - l.Error("failed to commit changes", "err", err) 347 + l.Error("failed to delete knot", "err", err) 254 348 fail() 255 349 return 256 350 } 257 351 258 352 err = k.Enforcer.E.SavePolicy() 259 353 if err != nil { 260 - l.Error("failed to update ACLs", "err", err) 261 - fail() 354 + l.Error("failed to update ACL", "err", err) 355 + k.Pages.HxRefresh(w) 262 356 return 263 357 } 264 358 265 - // add this knot to knotstream 266 - go k.Knotstream.AddSource( 267 - context.Background(), 268 - eventconsumer.NewKnotSource(domain), 269 - ) 359 + shouldRedirect := r.Header.Get("shouldRedirect") 360 + if shouldRedirect == "true" { 361 + k.Pages.HxRedirect(w, "/knots") 362 + return 363 + } 270 364 271 - k.Pages.KnotListing(w, pages.KnotListingParams{ 272 - Registration: *reg, 273 - }) 365 + w.Write([]byte{}) 274 366 } 275 367 276 - func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 277 - l := k.Logger.With("handler", "dashboard") 368 + func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { 369 + user := k.OAuth.GetUser(r) 370 + l := k.Logger.With("handler", "retry") 371 + 372 + noticeId := "operation-error" 373 + defaultErr := "Failed to verify knot. Try again later." 278 374 fail := func() { 279 - w.WriteHeader(http.StatusInternalServerError) 375 + k.Pages.Notice(w, noticeId, defaultErr) 280 376 } 281 377 282 378 domain := chi.URLParam(r, "domain") 283 379 if domain == "" { 284 - http.Error(w, "malformed url", http.StatusBadRequest) 380 + l.Error("empty domain") 381 + fail() 285 382 return 286 383 } 287 384 l = l.With("domain", domain) 288 - 289 - user := k.OAuth.GetUser(r) 290 - l = l.With("did", user.Did) 385 + l = l.With("user", user.Did) 291 386 292 - // dashboard is only available to owners 293 - ok, err := k.Enforcer.IsKnotOwner(user.Did, domain) 387 + registration, err := db.RegistrationByDomain(k.Db, domain) 294 388 if err != nil { 295 - l.Error("failed to query enforcer", "err", err) 389 + l.Error("failed to retrieve domain registration", "err", err) 296 390 fail() 297 - } 298 - if !ok { 299 - http.Error(w, "only owners can view dashboards", http.StatusUnauthorized) 300 391 return 301 392 } 302 393 303 - reg, err := db.RegistrationByDomain(k.Db, domain) 304 - if err != nil { 305 - l.Error("failed to get registration by domain", "err", err) 306 - fail() 394 + if registration.ByDid != user.Did { 395 + l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 396 + k.Pages.Notice(w, noticeId, "Failed to verify knot, unauthorized verification attempt.") 307 397 return 308 398 } 309 399 310 - var members []string 311 - if reg.Registered != nil { 312 - members, err = k.Enforcer.GetUserByRole("server:member", domain) 313 - if err != nil { 314 - l.Error("failed to get members list", "err", err) 315 - fail() 400 + // begin verification 401 + err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 402 + if err != nil { 403 + l.Error("verification failed", "err", err) 404 + 405 + if errors.Is(err, serververify.FetchError) { 406 + k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") 407 + return 408 + } 409 + 410 + if e, ok := err.(*serververify.OwnerMismatch); ok { 411 + k.Pages.Notice(w, noticeId, e.Error()) 316 412 return 317 413 } 318 - } 319 414 320 - repos, err := db.GetRepos( 321 - k.Db, 322 - 0, 323 - db.FilterEq("knot", domain), 324 - db.FilterIn("did", members), 325 - ) 326 - if err != nil { 327 - l.Error("failed to get repos list", "err", err) 328 415 fail() 329 416 return 330 417 } 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 418 337 - var didsToResolve []string 338 - for _, m := range members { 339 - didsToResolve = append(didsToResolve, m) 340 - } 341 - didsToResolve = append(didsToResolve, reg.ByDid) 342 - resolvedIds := k.IdResolver.ResolveIdents(r.Context(), didsToResolve) 343 - didHandleMap := make(map[string]string) 344 - for _, identity := range resolvedIds { 345 - if !identity.Handle.IsInvalidHandle() { 346 - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 347 - } else { 348 - didHandleMap[identity.DID.String()] = identity.DID.String() 349 - } 419 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 420 + if err != nil { 421 + l.Error("failed to mark verified", "err", err) 422 + k.Pages.Notice(w, noticeId, err.Error()) 423 + return 350 424 } 351 425 352 - k.Pages.Knot(w, pages.KnotParams{ 353 - LoggedInUser: user, 354 - DidHandleMap: didHandleMap, 355 - Registration: reg, 356 - Members: members, 357 - Repos: repoByMember, 358 - IsOwner: true, 359 - }) 360 - } 426 + // add this knot to knotstream 427 + go k.Knotstream.AddSource( 428 + r.Context(), 429 + eventconsumer.NewKnotSource(domain), 430 + ) 361 431 362 - // list members of domain, requires auth and requires owner status 363 - func (k *Knots) members(w http.ResponseWriter, r *http.Request) { 364 - l := k.Logger.With("handler", "members") 365 - 366 - domain := chi.URLParam(r, "domain") 367 - if domain == "" { 368 - http.Error(w, "malformed url", http.StatusBadRequest) 432 + shouldRefresh := r.Header.Get("shouldRefresh") 433 + if shouldRefresh == "true" { 434 + k.Pages.HxRefresh(w) 369 435 return 370 436 } 371 - l = l.With("domain", domain) 372 437 373 - // list all members for this domain 374 - memberDids, err := k.Enforcer.GetUserByRole("server:member", domain) 438 + // Get updated registration to show 439 + updatedRegistration, err := db.RegistrationByDomain(k.Db, domain) 375 440 if err != nil { 376 - w.Write([]byte("failed to fetch member list")) 441 + l.Error("failed get updated registration", "err", err) 442 + k.Pages.HxRefresh(w) 377 443 return 378 444 } 379 445 380 - w.Write([]byte(strings.Join(memberDids, "\n"))) 446 + w.Header().Set("HX-Reswap", "outerHTML") 447 + k.Pages.KnotListing(w, pages.KnotListingParams{ 448 + Registration: *updatedRegistration, 449 + }) 381 450 } 382 451 383 - // add member to domain, requires auth and requires invite access 384 452 func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 385 - l := k.Logger.With("handler", "members") 453 + user := k.OAuth.GetUser(r) 454 + l := k.Logger.With("handler", "addMember") 386 455 387 456 domain := chi.URLParam(r, "domain") 388 457 if domain == "" { 389 - http.Error(w, "malformed url", http.StatusBadRequest) 458 + l.Error("empty domain") 459 + http.Error(w, "Not found", http.StatusNotFound) 390 460 return 391 461 } 392 462 l = l.With("domain", domain) 463 + l = l.With("user", user.Did) 393 464 394 - reg, err := db.RegistrationByDomain(k.Db, domain) 465 + registration, err := db.RegistrationByDomain(k.Db, domain) 395 466 if err != nil { 396 - l.Error("failed to get registration by domain", "err", err) 397 - http.Error(w, "malformed url", http.StatusBadRequest) 467 + l.Error("failed to retrieve domain registration", "err", err) 468 + http.Error(w, "Not found", http.StatusNotFound) 398 469 return 399 470 } 400 471 401 - noticeId := fmt.Sprintf("add-member-error-%d", reg.Id) 402 - l = l.With("notice-id", noticeId) 472 + noticeId := fmt.Sprintf("add-member-error-%d", registration.Id) 403 473 defaultErr := "Failed to add member. Try again later." 404 474 fail := func() { 405 475 k.Pages.Notice(w, noticeId, defaultErr) 406 476 } 407 477 408 - subjectIdentifier := r.FormValue("subject") 409 - if subjectIdentifier == "" { 410 - http.Error(w, "malformed form", http.StatusBadRequest) 478 + if registration.ByDid != user.Did { 479 + l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 480 + k.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 411 481 return 412 482 } 413 - l = l.With("subjectIdentifier", subjectIdentifier) 414 483 415 - subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier) 484 + member := r.FormValue("member") 485 + if member == "" { 486 + l.Error("empty member") 487 + k.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 488 + return 489 + } 490 + l = l.With("member", member) 491 + 492 + memberId, err := k.IdResolver.ResolveIdent(r.Context(), member) 416 493 if err != nil { 417 - l.Error("failed to resolve identity", "err", err) 494 + l.Error("failed to resolve member identity to handle", "err", err) 495 + k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 496 + return 497 + } 498 + if memberId.Handle.IsInvalidHandle() { 499 + l.Error("failed to resolve member identity to handle") 418 500 k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 419 501 return 420 502 } 421 - l = l.With("subjectDid", subjectIdentity.DID) 422 503 423 - l.Info("adding member to knot") 424 - 425 - // announce this relation into the firehose, store into owners' pds 504 + // write to pds 426 505 client, err := k.OAuth.AuthorizedClient(r) 427 506 if err != nil { 428 - l.Error("failed to create client", "err", err) 507 + l.Error("failed to authorize client", "err", err) 429 508 fail() 430 509 return 431 510 } 432 511 433 - currentUser := k.OAuth.GetUser(r) 434 - createdAt := time.Now().Format(time.RFC3339) 435 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 512 + rkey := tid.TID() 513 + 514 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 436 515 Collection: tangled.KnotMemberNSID, 437 - Repo: currentUser.Did, 438 - Rkey: tid.TID(), 516 + Repo: user.Did, 517 + Rkey: rkey, 439 518 Record: &lexutil.LexiconTypeDecoder{ 440 519 Val: &tangled.KnotMember{ 441 - Subject: subjectIdentity.DID.String(), 520 + CreatedAt: time.Now().Format(time.RFC3339), 442 521 Domain: domain, 443 - CreatedAt: createdAt, 444 - }}, 522 + Subject: memberId.DID.String(), 523 + }, 524 + }, 445 525 }) 446 - // invalid record 447 526 if err != nil { 448 - l.Error("failed to write to PDS", "err", err) 527 + l.Error("failed to add record to PDS", "err", err) 528 + k.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") 529 + return 530 + } 531 + 532 + err = k.Enforcer.AddKnotMember(domain, memberId.DID.String()) 533 + if err != nil { 534 + l.Error("failed to add member to ACLs", "err", err) 449 535 fail() 450 536 return 451 537 } 452 - l = l.With("at-uri", resp.Uri) 453 - l.Info("wrote record to PDS") 454 538 455 - secret, err := db.GetRegistrationKey(k.Db, domain) 539 + err = k.Enforcer.E.SavePolicy() 456 540 if err != nil { 457 - l.Error("failed to get registration key", "err", err) 541 + l.Error("failed to save ACL policy", "err", err) 542 + fail() 543 + return 544 + } 545 + 546 + // success 547 + k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) 548 + } 549 + 550 + func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 551 + user := k.OAuth.GetUser(r) 552 + l := k.Logger.With("handler", "removeMember") 553 + 554 + noticeId := "operation-error" 555 + defaultErr := "Failed to remove member. Try again later." 556 + fail := func() { 557 + k.Pages.Notice(w, noticeId, defaultErr) 558 + } 559 + 560 + domain := chi.URLParam(r, "domain") 561 + if domain == "" { 562 + l.Error("empty domain") 458 563 fail() 459 564 return 460 565 } 566 + l = l.With("domain", domain) 567 + l = l.With("user", user.Did) 461 568 462 - ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) 569 + registration, err := db.RegistrationByDomain(k.Db, domain) 463 570 if err != nil { 464 - l.Error("failed to create client", "err", err) 571 + l.Error("failed to retrieve domain registration", "err", err) 465 572 fail() 466 573 return 467 574 } 468 575 469 - ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) 576 + if registration.ByDid != user.Did { 577 + l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid) 578 + k.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 579 + return 580 + } 581 + 582 + member := r.FormValue("member") 583 + if member == "" { 584 + l.Error("empty member") 585 + k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.") 586 + return 587 + } 588 + l = l.With("member", member) 589 + 590 + memberId, err := k.IdResolver.ResolveIdent(r.Context(), member) 470 591 if err != nil { 471 - l.Error("failed to reach knotserver", "err", err) 472 - k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.") 592 + l.Error("failed to resolve member identity to handle", "err", err) 593 + k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 594 + return 595 + } 596 + if memberId.Handle.IsInvalidHandle() { 597 + l.Error("failed to resolve member identity to handle") 598 + k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 473 599 return 474 600 } 475 601 476 - if ksResp.StatusCode != http.StatusNoContent { 477 - l.Error("status mismatch", "got", ksResp.StatusCode, "expected", http.StatusNoContent) 478 - k.Pages.Notice(w, noticeId, fmt.Sprintf("Unexpected status code from knotserver %d, expected %d", ksResp.StatusCode, http.StatusNoContent)) 602 + // remove from enforcer 603 + err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String()) 604 + if err != nil { 605 + l.Error("failed to update ACLs", "err", err) 606 + fail() 479 607 return 480 608 } 481 609 482 - err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) 610 + client, err := k.OAuth.AuthorizedClient(r) 483 611 if err != nil { 484 - l.Error("failed to add member to enforcer", "err", err) 612 + l.Error("failed to authorize client", "err", err) 485 613 fail() 486 614 return 487 615 } 488 616 489 - // success 490 - k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) 491 - } 617 + // TODO: We need to track the rkey for knot members to delete the record 618 + // For now, just remove from ACLs 619 + _ = client 492 620 493 - func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 621 + // commit everything 622 + err = k.Enforcer.E.SavePolicy() 623 + if err != nil { 624 + l.Error("failed to save ACLs", "err", err) 625 + fail() 626 + return 627 + } 628 + 629 + // ok 630 + k.Pages.HxRefresh(w) 494 631 }
+160
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.Register(tx, domain) 133 + if err != nil { 134 + return fmt.Errorf("failed to register domain: %w", err) 135 + } 136 + 137 + // add basic acls for this domain 138 + err = e.AddKnot(domain) 139 + if err != nil { 140 + return fmt.Errorf("failed to add knot to enforcer: %w", err) 141 + } 142 + 143 + // add this did as owner of this domain 144 + err = e.AddKnotOwner(domain, owner) 145 + if err != nil { 146 + return fmt.Errorf("failed to add knot owner to enforcer: %w", err) 147 + } 148 + 149 + err = tx.Commit() 150 + if err != nil { 151 + return fmt.Errorf("failed to commit changes: %w", err) 152 + } 153 + 154 + err = e.E.SavePolicy() 155 + if err != nil { 156 + return fmt.Errorf("failed to update ACLs: %w", err) 157 + } 158 + 159 + return nil 160 + }
+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" ··· 240 240 } 241 241 242 242 // begin verification 243 - err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 243 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 244 244 if err != nil { 245 245 l.Error("verification failed", "err", err) 246 246 s.Pages.HxRefresh(w) 247 247 return 248 248 } 249 249 250 - _, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 250 + _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 251 251 if err != nil { 252 252 l.Error("failed to mark verified", "err", err) 253 253 s.Pages.HxRefresh(w) ··· 413 413 } 414 414 415 415 // begin verification 416 - err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 416 + err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 417 417 if err != nil { 418 418 l.Error("verification failed", "err", err) 419 419 420 - if errors.Is(err, verify.FetchError) { 421 - s.Pages.Notice(w, noticeId, err.Error()) 420 + if errors.Is(err, serververify.FetchError) { 421 + s.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.") 422 422 return 423 423 } 424 424 425 - if e, ok := err.(*verify.OwnerMismatch); ok { 425 + if e, ok := err.(*serververify.OwnerMismatch); ok { 426 426 s.Pages.Notice(w, noticeId, e.Error()) 427 427 return 428 428 } ··· 431 431 return 432 432 } 433 433 434 - rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 434 + rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 435 435 if err != nil { 436 436 l.Error("failed to mark verified", "err", err) 437 437 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
··· 136 136 137 137 r.Mount("/settings", s.SettingsRouter()) 138 138 r.Mount("/strings", s.StringsRouter(mw)) 139 - r.Mount("/knots", s.KnotsRouter(mw)) 139 + r.Mount("/knots", s.KnotsRouter()) 140 140 r.Mount("/spindles", s.SpindlesRouter()) 141 141 r.Mount("/signup", s.SignupRouter()) 142 142 r.Mount("/", s.OAuthRouter()) ··· 184 184 return spindles.Router() 185 185 } 186 186 187 - func (s *State) KnotsRouter(mw *middleware.Middleware) http.Handler { 187 + func (s *State) KnotsRouter() http.Handler { 188 188 logger := log.New("knots") 189 189 190 190 knots := &knots.Knots{ ··· 198 198 Logger: logger, 199 199 } 200 200 201 - return knots.Router(mw) 201 + return knots.Router() 202 202 } 203 203 204 204 func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler {