Monorepo for Tangled tangled.org

appview: split off spindle verification logic into separate pkg

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 50cc4da6 460e1816

verified
Changed files
+560 -266
appview
spindleresolver
spindles
spindleverify
-176
appview/spindleresolver/resolver.go
··· 1 - package spindleresolver 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "errors" 7 - "fmt" 8 - "io" 9 - "net/http" 10 - "strings" 11 - "time" 12 - 13 - "tangled.sh/tangled.sh/core/api/tangled" 14 - "tangled.sh/tangled.sh/core/appview/cache" 15 - "tangled.sh/tangled.sh/core/appview/idresolver" 16 - 17 - "github.com/bluesky-social/indigo/api/atproto" 18 - "github.com/bluesky-social/indigo/xrpc" 19 - ) 20 - 21 - type ResolutionStatus string 22 - 23 - const ( 24 - StatusOK ResolutionStatus = "ok" 25 - StatusError ResolutionStatus = "error" 26 - StatusInvalid ResolutionStatus = "invalid" 27 - ) 28 - 29 - type Resolution struct { 30 - Status ResolutionStatus `json:"status"` 31 - OwnerDID string `json:"ownerDid,omitempty"` 32 - VerifiedAt time.Time `json:"verifiedAt"` 33 - } 34 - 35 - type Resolver struct { 36 - cache *cache.Cache 37 - http *http.Client 38 - config Config 39 - idResolver *idresolver.Resolver 40 - } 41 - 42 - type Config struct { 43 - HitTTL time.Duration 44 - ErrTTL time.Duration 45 - InvalidTTL time.Duration 46 - Dev bool 47 - } 48 - 49 - func NewResolver(cache *cache.Cache, client *http.Client, config Config) *Resolver { 50 - if client == nil { 51 - client = &http.Client{ 52 - Timeout: 2 * time.Second, 53 - } 54 - } 55 - return &Resolver{ 56 - cache: cache, 57 - http: client, 58 - config: config, 59 - } 60 - } 61 - 62 - func DefaultResolver(cache *cache.Cache) *Resolver { 63 - return NewResolver( 64 - cache, 65 - &http.Client{ 66 - Timeout: 2 * time.Second, 67 - }, 68 - Config{ 69 - HitTTL: 24 * time.Hour, 70 - ErrTTL: 30 * time.Second, 71 - InvalidTTL: 1 * time.Minute, 72 - }, 73 - ) 74 - } 75 - 76 - func (r *Resolver) ResolveInstance(ctx context.Context, domain string) (*Resolution, error) { 77 - key := "spindle:" + domain 78 - 79 - val, err := r.cache.Get(ctx, key).Result() 80 - if err == nil { 81 - var cached Resolution 82 - if err := json.Unmarshal([]byte(val), &cached); err == nil { 83 - return &cached, nil 84 - } 85 - } 86 - 87 - resolution, ttl := r.verify(ctx, domain) 88 - 89 - data, _ := json.Marshal(resolution) 90 - r.cache.Set(ctx, key, data, ttl) 91 - 92 - if resolution.Status == StatusOK { 93 - return resolution, nil 94 - } 95 - 96 - return resolution, fmt.Errorf("verification failed: %s", resolution.Status) 97 - } 98 - 99 - func (r *Resolver) verify(ctx context.Context, domain string) (*Resolution, time.Duration) { 100 - owner, err := r.fetchOwner(ctx, domain) 101 - if err != nil { 102 - return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL 103 - } 104 - 105 - record, err := r.fetchRecord(ctx, owner, domain) 106 - if err != nil { 107 - return &Resolution{Status: StatusError, VerifiedAt: time.Now()}, r.config.ErrTTL 108 - } 109 - 110 - if record.Instance == domain { 111 - return &Resolution{ 112 - Status: StatusOK, 113 - OwnerDID: owner, 114 - VerifiedAt: time.Now(), 115 - }, r.config.HitTTL 116 - } 117 - 118 - return &Resolution{ 119 - Status: StatusInvalid, 120 - OwnerDID: owner, 121 - VerifiedAt: time.Now(), 122 - }, r.config.InvalidTTL 123 - } 124 - 125 - func (r *Resolver) fetchOwner(ctx context.Context, domain string) (string, error) { 126 - scheme := "https" 127 - if r.config.Dev { 128 - scheme = "http" 129 - } 130 - 131 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 132 - req, err := http.NewRequest("GET", url, nil) 133 - if err != nil { 134 - return "", err 135 - } 136 - 137 - resp, err := r.http.Do(req.WithContext(ctx)) 138 - if err != nil || resp.StatusCode != 200 { 139 - return "", errors.New("failed to fetch /owner") 140 - } 141 - 142 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 143 - if err != nil { 144 - return "", fmt.Errorf("failed to read /owner response: %w", err) 145 - } 146 - 147 - did := strings.TrimSpace(string(body)) 148 - if did == "" { 149 - return "", errors.New("empty DID in /owner response") 150 - } 151 - 152 - return did, nil 153 - } 154 - 155 - func (r *Resolver) fetchRecord(ctx context.Context, did, rkey string) (*tangled.Spindle, error) { 156 - ident, err := r.idResolver.ResolveIdent(ctx, did) 157 - if err != nil { 158 - return nil, err 159 - } 160 - 161 - xrpcc := xrpc.Client{ 162 - Host: ident.PDSEndpoint(), 163 - } 164 - 165 - rec, err := atproto.RepoGetRecord(ctx, &xrpcc, "", tangled.SpindleNSID, did, rkey) 166 - if err != nil { 167 - return nil, err 168 - } 169 - 170 - out, ok := rec.Value.Val.(*tangled.Spindle) 171 - if !ok { 172 - return nil, fmt.Errorf("invalid record returned") 173 - } 174 - 175 - return out, nil 176 - }
+442 -90
appview/spindles/spindles.go
··· 1 1 package spindles 2 2 3 3 import ( 4 - "context" 5 4 "errors" 6 5 "fmt" 7 - "io" 8 6 "log/slog" 9 7 "net/http" 10 - "strings" 8 + "slices" 11 9 "time" 12 10 13 11 "github.com/go-chi/chi/v5" 14 12 "tangled.sh/tangled.sh/core/api/tangled" 13 + "tangled.sh/tangled.sh/core/appview" 15 14 "tangled.sh/tangled.sh/core/appview/config" 16 15 "tangled.sh/tangled.sh/core/appview/db" 16 + "tangled.sh/tangled.sh/core/appview/idresolver" 17 17 "tangled.sh/tangled.sh/core/appview/middleware" 18 18 "tangled.sh/tangled.sh/core/appview/oauth" 19 19 "tangled.sh/tangled.sh/core/appview/pages" 20 + verify "tangled.sh/tangled.sh/core/appview/spindleverify" 20 21 "tangled.sh/tangled.sh/core/rbac" 21 22 22 23 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 25 26 ) 26 27 27 28 type Spindles struct { 28 - Db *db.DB 29 - OAuth *oauth.OAuth 30 - Pages *pages.Pages 31 - Config *config.Config 32 - Enforcer *rbac.Enforcer 33 - Logger *slog.Logger 29 + Db *db.DB 30 + OAuth *oauth.OAuth 31 + Pages *pages.Pages 32 + Config *config.Config 33 + Enforcer *rbac.Enforcer 34 + IdResolver *idresolver.Resolver 35 + Logger *slog.Logger 34 36 } 35 37 36 38 func (s *Spindles) Router() http.Handler { 37 39 r := chi.NewRouter() 38 40 39 - r.Use(middleware.AuthMiddleware(s.OAuth)) 41 + r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles) 42 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register) 40 43 41 - r.Get("/", s.spindles) 42 - r.Post("/register", s.register) 43 - r.Delete("/{instance}", s.delete) 44 - r.Post("/{instance}/retry", s.retry) 44 + r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard) 45 + r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete) 46 + 47 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry) 48 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember) 49 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember) 45 50 46 51 return r 47 52 } ··· 64 69 }) 65 70 } 66 71 72 + func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 73 + l := s.Logger.With("handler", "dashboard") 74 + 75 + user := s.OAuth.GetUser(r) 76 + l = l.With("user", user.Did) 77 + 78 + instance := chi.URLParam(r, "instance") 79 + if instance == "" { 80 + return 81 + } 82 + l = l.With("instance", instance) 83 + 84 + spindles, err := db.GetSpindles( 85 + s.Db, 86 + db.FilterEq("instance", instance), 87 + db.FilterEq("owner", user.Did), 88 + db.FilterIsNot("verified", "null"), 89 + ) 90 + if err != nil || len(spindles) != 1 { 91 + l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) 92 + http.Error(w, "Not found", http.StatusNotFound) 93 + return 94 + } 95 + 96 + spindle := spindles[0] 97 + members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance) 98 + if err != nil { 99 + l.Error("failed to get spindle members", "err", err) 100 + http.Error(w, "Not found", http.StatusInternalServerError) 101 + return 102 + } 103 + slices.Sort(members) 104 + 105 + repos, err := db.GetRepos( 106 + s.Db, 107 + db.FilterEq("spindle", instance), 108 + ) 109 + if err != nil { 110 + l.Error("failed to get spindle repos", "err", err) 111 + http.Error(w, "Not found", http.StatusInternalServerError) 112 + return 113 + } 114 + 115 + identsToResolve := make([]string, len(members)) 116 + for i, member := range members { 117 + identsToResolve[i] = member 118 + } 119 + resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve) 120 + didHandleMap := make(map[string]string) 121 + for _, identity := range resolvedIds { 122 + if !identity.Handle.IsInvalidHandle() { 123 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 124 + } else { 125 + didHandleMap[identity.DID.String()] = identity.DID.String() 126 + } 127 + } 128 + 129 + // organize repos by did 130 + repoMap := make(map[string][]db.Repo) 131 + for _, r := range repos { 132 + repoMap[r.Did] = append(repoMap[r.Did], r) 133 + } 134 + 135 + s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{ 136 + LoggedInUser: user, 137 + Spindle: spindle, 138 + Members: members, 139 + Repos: repoMap, 140 + DidHandleMap: didHandleMap, 141 + }) 142 + } 143 + 67 144 // this endpoint inserts a record on behalf of the user to register that domain 68 145 // 69 146 // when registered, it also makes a request to see if the spindle declares this users as its owner, ··· 85 162 s.Pages.Notice(w, noticeId, "Incomplete form.") 86 163 return 87 164 } 165 + l = l.With("instance", instance) 166 + l = l.With("user", user.Did) 88 167 89 168 tx, err := s.Db.Begin() 90 169 if err != nil { ··· 92 171 fail() 93 172 return 94 173 } 95 - defer tx.Rollback() 174 + defer func() { 175 + tx.Rollback() 176 + s.Enforcer.E.LoadPolicy() 177 + }() 96 178 97 179 err = db.AddSpindle(tx, db.Spindle{ 98 180 Owner: syntax.DID(user.Did), ··· 104 186 return 105 187 } 106 188 189 + err = s.Enforcer.AddSpindle(instance) 190 + if err != nil { 191 + l.Error("failed to create spindle", "err", err) 192 + fail() 193 + return 194 + } 195 + 107 196 // create record on pds 108 197 client, err := s.OAuth.AuthorizedClient(r) 109 198 if err != nil { ··· 144 233 return 145 234 } 146 235 147 - // begin verification 148 - expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 149 - if err != nil { 150 - l.Error("verification failed", "err", err) 151 - 152 - // just refresh the page 153 - s.Pages.HxRefresh(w) 154 - return 155 - } 156 - 157 - if expectedOwner != user.Did { 158 - // verification failed 159 - l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 160 - s.Pages.HxRefresh(w) 161 - return 162 - } 163 - 164 - tx, err = s.Db.Begin() 165 - if err != nil { 166 - l.Error("failed to commit verification info", "err", err) 167 - s.Pages.HxRefresh(w) 168 - return 169 - } 170 - defer func() { 171 - tx.Rollback() 172 - s.Enforcer.E.LoadPolicy() 173 - }() 174 - 175 - // mark this spindle as verified in the db 176 - _, err = db.VerifySpindle( 177 - tx, 178 - db.FilterEq("owner", user.Did), 179 - db.FilterEq("instance", instance), 180 - ) 181 - 182 - err = s.Enforcer.AddSpindleOwner(instance, user.Did) 236 + err = s.Enforcer.E.SavePolicy() 183 237 if err != nil { 184 238 l.Error("failed to update ACL", "err", err) 185 239 s.Pages.HxRefresh(w) 186 240 return 187 241 } 188 242 189 - err = tx.Commit() 243 + // begin verification 244 + err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 190 245 if err != nil { 191 - l.Error("failed to commit verification info", "err", err) 246 + l.Error("verification failed", "err", err) 192 247 s.Pages.HxRefresh(w) 193 248 return 194 249 } 195 250 196 - err = s.Enforcer.E.SavePolicy() 251 + _, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 197 252 if err != nil { 198 - l.Error("failed to update ACL", "err", err) 253 + l.Error("failed to mark verified", "err", err) 199 254 s.Pages.HxRefresh(w) 200 255 return 201 256 } ··· 207 262 208 263 func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 209 264 user := s.OAuth.GetUser(r) 210 - l := s.Logger.With("handler", "register") 265 + l := s.Logger.With("handler", "delete") 211 266 212 267 noticeId := "operation-error" 213 268 defaultErr := "Failed to delete spindle. Try again later." ··· 222 277 return 223 278 } 224 279 280 + spindles, err := db.GetSpindles( 281 + s.Db, 282 + db.FilterEq("owner", user.Did), 283 + db.FilterEq("instance", instance), 284 + ) 285 + if err != nil || len(spindles) != 1 { 286 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 287 + fail() 288 + return 289 + } 290 + 291 + if string(spindles[0].Owner) != user.Did { 292 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 293 + s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 294 + return 295 + } 296 + 225 297 tx, err := s.Db.Begin() 226 298 if err != nil { 227 299 l.Error("failed to start txn", "err", err) 228 300 fail() 229 301 return 230 302 } 231 - defer tx.Rollback() 303 + defer func() { 304 + tx.Rollback() 305 + s.Enforcer.E.LoadPolicy() 306 + }() 232 307 233 308 err = db.DeleteSpindle( 234 309 tx, ··· 237 312 ) 238 313 if err != nil { 239 314 l.Error("failed to delete spindle", "err", err) 315 + fail() 316 + return 317 + } 318 + 319 + err = s.Enforcer.RemoveSpindle(instance) 320 + if err != nil { 321 + l.Error("failed to update ACL", "err", err) 240 322 fail() 241 323 return 242 324 } ··· 265 347 return 266 348 } 267 349 350 + err = s.Enforcer.E.SavePolicy() 351 + if err != nil { 352 + l.Error("failed to update ACL", "err", err) 353 + s.Pages.HxRefresh(w) 354 + return 355 + } 356 + 357 + shouldRedirect := r.Header.Get("shouldRedirect") 358 + if shouldRedirect == "true" { 359 + s.Pages.HxRedirect(w, "/spindles") 360 + return 361 + } 362 + 268 363 w.Write([]byte{}) 269 364 } 270 365 271 366 func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 272 367 user := s.OAuth.GetUser(r) 273 - l := s.Logger.With("handler", "register") 368 + l := s.Logger.With("handler", "retry") 274 369 275 370 noticeId := "operation-error" 276 371 defaultErr := "Failed to verify spindle. Try again later." ··· 284 379 fail() 285 380 return 286 381 } 382 + l = l.With("instance", instance) 383 + l = l.With("user", user.Did) 384 + 385 + spindles, err := db.GetSpindles( 386 + s.Db, 387 + db.FilterEq("owner", user.Did), 388 + db.FilterEq("instance", instance), 389 + ) 390 + if err != nil || len(spindles) != 1 { 391 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 392 + fail() 393 + return 394 + } 395 + 396 + if string(spindles[0].Owner) != user.Did { 397 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 398 + s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 399 + return 400 + } 287 401 288 402 // begin verification 289 - expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 403 + err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 290 404 if err != nil { 291 405 l.Error("verification failed", "err", err) 406 + 407 + if errors.Is(err, verify.FetchError) { 408 + s.Pages.Notice(w, noticeId, err.Error()) 409 + return 410 + } 411 + 412 + if e, ok := err.(*verify.OwnerMismatch); ok { 413 + s.Pages.Notice(w, noticeId, e.Error()) 414 + return 415 + } 416 + 292 417 fail() 293 418 return 294 419 } 295 420 296 - if expectedOwner != user.Did { 297 - l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 298 - s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did)) 421 + rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 422 + if err != nil { 423 + l.Error("failed to mark verified", "err", err) 424 + s.Pages.Notice(w, noticeId, err.Error()) 299 425 return 300 426 } 301 427 302 - // mark this spindle as verified in the db 303 - rowId, err := db.VerifySpindle( 428 + verifiedSpindle, err := db.GetSpindles( 429 + s.Db, 430 + db.FilterEq("id", rowId), 431 + ) 432 + if err != nil || len(verifiedSpindle) != 1 { 433 + l.Error("failed get new spindle", "err", err) 434 + s.Pages.HxRefresh(w) 435 + return 436 + } 437 + 438 + shouldRefresh := r.Header.Get("shouldRefresh") 439 + if shouldRefresh == "true" { 440 + s.Pages.HxRefresh(w) 441 + return 442 + } 443 + 444 + w.Header().Set("HX-Reswap", "outerHTML") 445 + s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]}) 446 + } 447 + 448 + func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 449 + user := s.OAuth.GetUser(r) 450 + l := s.Logger.With("handler", "addMember") 451 + 452 + instance := chi.URLParam(r, "instance") 453 + if instance == "" { 454 + l.Error("empty instance") 455 + http.Error(w, "Not found", http.StatusNotFound) 456 + return 457 + } 458 + l = l.With("instance", instance) 459 + l = l.With("user", user.Did) 460 + 461 + spindles, err := db.GetSpindles( 304 462 s.Db, 305 463 db.FilterEq("owner", user.Did), 306 464 db.FilterEq("instance", instance), 307 465 ) 466 + if err != nil || len(spindles) != 1 { 467 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 468 + http.Error(w, "Not found", http.StatusNotFound) 469 + return 470 + } 471 + 472 + noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id) 473 + defaultErr := "Failed to add member. Try again later." 474 + fail := func() { 475 + s.Pages.Notice(w, noticeId, defaultErr) 476 + } 477 + 478 + if string(spindles[0].Owner) != user.Did { 479 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 480 + s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 481 + return 482 + } 483 + 484 + member := r.FormValue("member") 485 + if member == "" { 486 + l.Error("empty member") 487 + s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 488 + return 489 + } 490 + l = l.With("member", member) 491 + 492 + memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 493 + if err != nil { 494 + l.Error("failed to resolve member identity to handle", "err", err) 495 + s.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") 500 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 501 + return 502 + } 503 + 504 + // write to pds 505 + client, err := s.OAuth.AuthorizedClient(r) 506 + if err != nil { 507 + l.Error("failed to authorize client", "err", err) 508 + fail() 509 + return 510 + } 511 + 512 + tx, err := s.Db.Begin() 308 513 if err != nil { 309 - l.Error("verification failed", "err", err) 514 + l.Error("failed to start txn", "err", err) 310 515 fail() 311 516 return 312 517 } 518 + defer func() { 519 + tx.Rollback() 520 + s.Enforcer.E.LoadPolicy() 521 + }() 313 522 314 - verifiedSpindle := db.Spindle{ 315 - Id: int(rowId), 316 - Owner: syntax.DID(user.Did), 523 + rkey := appview.TID() 524 + 525 + // add member to db 526 + if err = db.AddSpindleMember(tx, db.SpindleMember{ 527 + Did: syntax.DID(user.Did), 528 + Rkey: rkey, 317 529 Instance: instance, 530 + Subject: memberId.DID, 531 + }); err != nil { 532 + l.Error("failed to add spindle member", "err", err) 533 + fail() 534 + return 318 535 } 319 536 320 - w.Header().Set("HX-Reswap", "outerHTML") 321 - s.Pages.SpindleListing(w, pages.SpindleListingParams{ 322 - LoggedInUser: user, 323 - Spindle: verifiedSpindle, 537 + if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil { 538 + l.Error("failed to add member to ACLs") 539 + fail() 540 + return 541 + } 542 + 543 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 544 + Collection: tangled.SpindleMemberNSID, 545 + Repo: user.Did, 546 + Rkey: rkey, 547 + Record: &lexutil.LexiconTypeDecoder{ 548 + Val: &tangled.SpindleMember{ 549 + CreatedAt: time.Now().Format(time.RFC3339), 550 + Instance: instance, 551 + Subject: memberId.DID.String(), 552 + }, 553 + }, 324 554 }) 555 + if err != nil { 556 + l.Error("failed to add record to PDS", "err", err) 557 + s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") 558 + return 559 + } 560 + 561 + if err = tx.Commit(); err != nil { 562 + l.Error("failed to commit txn", "err", err) 563 + fail() 564 + return 565 + } 566 + 567 + if err = s.Enforcer.E.SavePolicy(); err != nil { 568 + l.Error("failed to add member to ACLs", "err", err) 569 + fail() 570 + return 571 + } 572 + 573 + // success 574 + s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance)) 325 575 } 326 576 327 - func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 328 - scheme := "https" 329 - if dev { 330 - scheme = "http" 577 + func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 578 + user := s.OAuth.GetUser(r) 579 + l := s.Logger.With("handler", "removeMember") 580 + 581 + noticeId := "operation-error" 582 + defaultErr := "Failed to add member. Try again later." 583 + fail := func() { 584 + s.Pages.Notice(w, noticeId, defaultErr) 331 585 } 332 586 333 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 334 - req, err := http.NewRequest("GET", url, nil) 587 + instance := chi.URLParam(r, "instance") 588 + if instance == "" { 589 + l.Error("empty instance") 590 + fail() 591 + return 592 + } 593 + l = l.With("instance", instance) 594 + l = l.With("user", user.Did) 595 + 596 + spindles, err := db.GetSpindles( 597 + s.Db, 598 + db.FilterEq("owner", user.Did), 599 + db.FilterEq("instance", instance), 600 + ) 601 + if err != nil || len(spindles) != 1 { 602 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 603 + fail() 604 + return 605 + } 606 + 607 + if string(spindles[0].Owner) != user.Did { 608 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 609 + s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 610 + return 611 + } 612 + 613 + member := r.FormValue("member") 614 + if member == "" { 615 + l.Error("empty member") 616 + s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 617 + return 618 + } 619 + l = l.With("member", member) 620 + 621 + memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 622 + if err != nil { 623 + l.Error("failed to resolve member identity to handle", "err", err) 624 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 625 + return 626 + } 627 + if memberId.Handle.IsInvalidHandle() { 628 + l.Error("failed to resolve member identity to handle") 629 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 630 + return 631 + } 632 + 633 + tx, err := s.Db.Begin() 335 634 if err != nil { 336 - return "", err 635 + l.Error("failed to start txn", "err", err) 636 + fail() 637 + return 638 + } 639 + defer func() { 640 + tx.Rollback() 641 + s.Enforcer.E.LoadPolicy() 642 + }() 643 + 644 + // get the record from the DB first: 645 + members, err := db.GetSpindleMembers( 646 + s.Db, 647 + db.FilterEq("did", user.Did), 648 + db.FilterEq("instance", instance), 649 + db.FilterEq("subject", memberId.DID), 650 + ) 651 + if err != nil || len(members) != 1 { 652 + l.Error("failed to get member", "err", err) 653 + fail() 654 + return 337 655 } 338 656 339 - client := &http.Client{ 340 - Timeout: 1 * time.Second, 657 + // remove from db 658 + if err = db.RemoveSpindleMember( 659 + tx, 660 + db.FilterEq("did", user.Did), 661 + db.FilterEq("instance", instance), 662 + db.FilterEq("subject", memberId.DID), 663 + ); err != nil { 664 + l.Error("failed to remove spindle member", "err", err) 665 + fail() 666 + return 341 667 } 342 668 343 - resp, err := client.Do(req.WithContext(ctx)) 344 - if err != nil || resp.StatusCode != 200 { 345 - return "", errors.New("failed to fetch /owner") 669 + // remove from enforcer 670 + if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil { 671 + l.Error("failed to update ACLs", "err", err) 672 + fail() 673 + return 346 674 } 347 675 348 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 676 + client, err := s.OAuth.AuthorizedClient(r) 349 677 if err != nil { 350 - return "", fmt.Errorf("failed to read /owner response: %w", err) 678 + l.Error("failed to authorize client", "err", err) 679 + fail() 680 + return 351 681 } 352 682 353 - did := strings.TrimSpace(string(body)) 354 - if did == "" { 355 - return "", errors.New("empty DID in /owner response") 683 + // remove from pds 684 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 685 + Collection: tangled.SpindleMemberNSID, 686 + Repo: user.Did, 687 + Rkey: members[0].Rkey, 688 + }) 689 + if err != nil { 690 + // non-fatal 691 + l.Error("failed to delete record", "err", err) 692 + } 693 + 694 + // commit everything 695 + if err = tx.Commit(); err != nil { 696 + l.Error("failed to commit txn", "err", err) 697 + fail() 698 + return 356 699 } 357 700 358 - return did, nil 701 + // commit everything 702 + if err = s.Enforcer.E.SavePolicy(); err != nil { 703 + l.Error("failed to save ACLs", "err", err) 704 + fail() 705 + return 706 + } 707 + 708 + // ok 709 + s.Pages.HxRefresh(w) 710 + return 359 711 }
+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 + }