Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview: split off spindle verification logic into separate pkg

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

oppiliappan 50cc4da6 460e1816

+585 -291
-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 - }
+467 -115
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" ··· 26 25 ) 27 26 28 27 type Spindles struct { 29 - Db *db.DB 30 - OAuth *oauth.OAuth 31 - Pages *pages.Pages 32 - Config *config.Config 33 - Enforcer *rbac.Enforcer 34 - Logger *slog.Logger 28 + Db *db.DB 29 + OAuth *oauth.OAuth 30 + Pages *pages.Pages 31 + Config *config.Config 32 + Enforcer *rbac.Enforcer 33 + IdResolver *idresolver.Resolver 34 + Logger *slog.Logger 35 35 } 36 36 37 37 func (s *Spindles) Router() http.Handler { 38 38 r := chi.NewRouter() 39 39 40 - r.Use(middleware.AuthMiddleware(s.OAuth)) 40 + r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles) 41 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register) 41 42 42 - r.Get("/", s.spindles) 43 - r.Post("/register", s.register) 44 - r.Delete("/{instance}", s.delete) 45 - r.Post("/{instance}/retry", s.retry) 43 + r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard) 44 + r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete) 45 + 46 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry) 47 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember) 48 + r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember) 46 49 47 50 return r 48 51 } ··· 66 61 s.Pages.Spindles(w, pages.SpindlesParams{ 67 62 LoggedInUser: user, 68 63 Spindles: all, 64 + }) 65 + } 66 + 67 + func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 68 + l := s.Logger.With("handler", "dashboard") 69 + 70 + user := s.OAuth.GetUser(r) 71 + l = l.With("user", user.Did) 72 + 73 + instance := chi.URLParam(r, "instance") 74 + if instance == "" { 75 + return 76 + } 77 + l = l.With("instance", instance) 78 + 79 + spindles, err := db.GetSpindles( 80 + s.Db, 81 + db.FilterEq("instance", instance), 82 + db.FilterEq("owner", user.Did), 83 + db.FilterIsNot("verified", "null"), 84 + ) 85 + if err != nil || len(spindles) != 1 { 86 + l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) 87 + http.Error(w, "Not found", http.StatusNotFound) 88 + return 89 + } 90 + 91 + spindle := spindles[0] 92 + members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance) 93 + if err != nil { 94 + l.Error("failed to get spindle members", "err", err) 95 + http.Error(w, "Not found", http.StatusInternalServerError) 96 + return 97 + } 98 + slices.Sort(members) 99 + 100 + repos, err := db.GetRepos( 101 + s.Db, 102 + db.FilterEq("spindle", instance), 103 + ) 104 + if err != nil { 105 + l.Error("failed to get spindle repos", "err", err) 106 + http.Error(w, "Not found", http.StatusInternalServerError) 107 + return 108 + } 109 + 110 + identsToResolve := make([]string, len(members)) 111 + for i, member := range members { 112 + identsToResolve[i] = member 113 + } 114 + resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve) 115 + didHandleMap := make(map[string]string) 116 + for _, identity := range resolvedIds { 117 + if !identity.Handle.IsInvalidHandle() { 118 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 119 + } else { 120 + didHandleMap[identity.DID.String()] = identity.DID.String() 121 + } 122 + } 123 + 124 + // organize repos by did 125 + repoMap := make(map[string][]db.Repo) 126 + for _, r := range repos { 127 + repoMap[r.Did] = append(repoMap[r.Did], r) 128 + } 129 + 130 + s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{ 131 + LoggedInUser: user, 132 + Spindle: spindle, 133 + Members: members, 134 + Repos: repoMap, 135 + DidHandleMap: didHandleMap, 69 136 }) 70 137 } 71 138 ··· 162 85 s.Pages.Notice(w, noticeId, "Incomplete form.") 163 86 return 164 87 } 88 + l = l.With("instance", instance) 89 + l = l.With("user", user.Did) 165 90 166 91 tx, err := s.Db.Begin() 167 92 if err != nil { ··· 171 92 fail() 172 93 return 173 94 } 174 - defer tx.Rollback() 95 + defer func() { 96 + tx.Rollback() 97 + s.Enforcer.E.LoadPolicy() 98 + }() 175 99 176 100 err = db.AddSpindle(tx, db.Spindle{ 177 101 Owner: syntax.DID(user.Did), ··· 182 100 }) 183 101 if err != nil { 184 102 l.Error("failed to insert", "err", err) 103 + fail() 104 + return 105 + } 106 + 107 + err = s.Enforcer.AddSpindle(instance) 108 + if err != nil { 109 + l.Error("failed to create spindle", "err", err) 185 110 fail() 186 111 return 187 112 } ··· 233 144 return 234 145 } 235 146 236 - // begin verification 237 - expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 238 - if err != nil { 239 - l.Error("verification failed", "err", err) 240 - 241 - // just refresh the page 242 - s.Pages.HxRefresh(w) 243 - return 244 - } 245 - 246 - if expectedOwner != user.Did { 247 - // verification failed 248 - l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 249 - s.Pages.HxRefresh(w) 250 - return 251 - } 252 - 253 - tx, err = s.Db.Begin() 254 - if err != nil { 255 - l.Error("failed to commit verification info", "err", err) 256 - s.Pages.HxRefresh(w) 257 - return 258 - } 259 - defer func() { 260 - tx.Rollback() 261 - s.Enforcer.E.LoadPolicy() 262 - }() 263 - 264 - // mark this spindle as verified in the db 265 - _, err = db.VerifySpindle( 266 - tx, 267 - db.FilterEq("owner", user.Did), 268 - db.FilterEq("instance", instance), 269 - ) 270 - 271 - err = s.Enforcer.AddSpindleOwner(instance, user.Did) 272 - if err != nil { 273 - l.Error("failed to update ACL", "err", err) 274 - s.Pages.HxRefresh(w) 275 - return 276 - } 277 - 278 - err = tx.Commit() 279 - if err != nil { 280 - l.Error("failed to commit verification info", "err", err) 281 - s.Pages.HxRefresh(w) 282 - return 283 - } 284 - 285 147 err = s.Enforcer.E.SavePolicy() 286 148 if err != nil { 287 149 l.Error("failed to update ACL", "err", err) 150 + s.Pages.HxRefresh(w) 151 + return 152 + } 153 + 154 + // begin verification 155 + err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 156 + if err != nil { 157 + l.Error("verification failed", "err", err) 158 + s.Pages.HxRefresh(w) 159 + return 160 + } 161 + 162 + _, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 163 + if err != nil { 164 + l.Error("failed to mark verified", "err", err) 288 165 s.Pages.HxRefresh(w) 289 166 return 290 167 } ··· 262 207 263 208 func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 264 209 user := s.OAuth.GetUser(r) 265 - l := s.Logger.With("handler", "register") 210 + l := s.Logger.With("handler", "delete") 266 211 267 212 noticeId := "operation-error" 268 213 defaultErr := "Failed to delete spindle. Try again later." ··· 277 222 return 278 223 } 279 224 225 + spindles, err := db.GetSpindles( 226 + s.Db, 227 + db.FilterEq("owner", user.Did), 228 + db.FilterEq("instance", instance), 229 + ) 230 + if err != nil || len(spindles) != 1 { 231 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 232 + fail() 233 + return 234 + } 235 + 236 + if string(spindles[0].Owner) != user.Did { 237 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 238 + s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 239 + return 240 + } 241 + 280 242 tx, err := s.Db.Begin() 281 243 if err != nil { 282 244 l.Error("failed to start txn", "err", err) 283 245 fail() 284 246 return 285 247 } 286 - defer tx.Rollback() 248 + defer func() { 249 + tx.Rollback() 250 + s.Enforcer.E.LoadPolicy() 251 + }() 287 252 288 253 err = db.DeleteSpindle( 289 254 tx, ··· 312 237 ) 313 238 if err != nil { 314 239 l.Error("failed to delete spindle", "err", err) 240 + fail() 241 + return 242 + } 243 + 244 + err = s.Enforcer.RemoveSpindle(instance) 245 + if err != nil { 246 + l.Error("failed to update ACL", "err", err) 315 247 fail() 316 248 return 317 249 } ··· 347 265 return 348 266 } 349 267 268 + err = s.Enforcer.E.SavePolicy() 269 + if err != nil { 270 + l.Error("failed to update ACL", "err", err) 271 + s.Pages.HxRefresh(w) 272 + return 273 + } 274 + 275 + shouldRedirect := r.Header.Get("shouldRedirect") 276 + if shouldRedirect == "true" { 277 + s.Pages.HxRedirect(w, "/spindles") 278 + return 279 + } 280 + 350 281 w.Write([]byte{}) 351 282 } 352 283 353 284 func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 354 285 user := s.OAuth.GetUser(r) 355 - l := s.Logger.With("handler", "register") 286 + l := s.Logger.With("handler", "retry") 356 287 357 288 noticeId := "operation-error" 358 289 defaultErr := "Failed to verify spindle. Try again later." ··· 379 284 fail() 380 285 return 381 286 } 287 + l = l.With("instance", instance) 288 + l = l.With("user", user.Did) 382 289 383 - // begin verification 384 - expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev) 385 - if err != nil { 386 - l.Error("verification failed", "err", err) 387 - fail() 388 - return 389 - } 390 - 391 - if expectedOwner != user.Did { 392 - l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did) 393 - s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did)) 394 - return 395 - } 396 - 397 - // mark this spindle as verified in the db 398 - rowId, err := db.VerifySpindle( 290 + spindles, err := db.GetSpindles( 399 291 s.Db, 400 292 db.FilterEq("owner", user.Did), 401 293 db.FilterEq("instance", instance), 402 294 ) 403 - if err != nil { 404 - l.Error("verification failed", "err", err) 295 + if err != nil || len(spindles) != 1 { 296 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 405 297 fail() 406 298 return 407 299 } 408 300 409 - verifiedSpindle := db.Spindle{ 410 - Id: int(rowId), 411 - Owner: syntax.DID(user.Did), 412 - Instance: instance, 301 + if string(spindles[0].Owner) != user.Did { 302 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 303 + s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 304 + return 305 + } 306 + 307 + // begin verification 308 + err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 309 + if err != nil { 310 + l.Error("verification failed", "err", err) 311 + 312 + if errors.Is(err, verify.FetchError) { 313 + s.Pages.Notice(w, noticeId, err.Error()) 314 + return 315 + } 316 + 317 + if e, ok := err.(*verify.OwnerMismatch); ok { 318 + s.Pages.Notice(w, noticeId, e.Error()) 319 + return 320 + } 321 + 322 + fail() 323 + return 324 + } 325 + 326 + rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did) 327 + if err != nil { 328 + l.Error("failed to mark verified", "err", err) 329 + s.Pages.Notice(w, noticeId, err.Error()) 330 + return 331 + } 332 + 333 + verifiedSpindle, err := db.GetSpindles( 334 + s.Db, 335 + db.FilterEq("id", rowId), 336 + ) 337 + if err != nil || len(verifiedSpindle) != 1 { 338 + l.Error("failed get new spindle", "err", err) 339 + s.Pages.HxRefresh(w) 340 + return 341 + } 342 + 343 + shouldRefresh := r.Header.Get("shouldRefresh") 344 + if shouldRefresh == "true" { 345 + s.Pages.HxRefresh(w) 346 + return 413 347 } 414 348 415 349 w.Header().Set("HX-Reswap", "outerHTML") 416 - s.Pages.SpindleListing(w, pages.SpindleListingParams{ 417 - LoggedInUser: user, 418 - Spindle: verifiedSpindle, 419 - }) 350 + s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]}) 420 351 } 421 352 422 - func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) { 423 - scheme := "https" 424 - if dev { 425 - scheme = "http" 353 + func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 354 + user := s.OAuth.GetUser(r) 355 + l := s.Logger.With("handler", "addMember") 356 + 357 + instance := chi.URLParam(r, "instance") 358 + if instance == "" { 359 + l.Error("empty instance") 360 + http.Error(w, "Not found", http.StatusNotFound) 361 + return 362 + } 363 + l = l.With("instance", instance) 364 + l = l.With("user", user.Did) 365 + 366 + spindles, err := db.GetSpindles( 367 + s.Db, 368 + db.FilterEq("owner", user.Did), 369 + db.FilterEq("instance", instance), 370 + ) 371 + if err != nil || len(spindles) != 1 { 372 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 373 + http.Error(w, "Not found", http.StatusNotFound) 374 + return 426 375 } 427 376 428 - url := fmt.Sprintf("%s://%s/owner", scheme, domain) 429 - req, err := http.NewRequest("GET", url, nil) 377 + noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id) 378 + defaultErr := "Failed to add member. Try again later." 379 + fail := func() { 380 + s.Pages.Notice(w, noticeId, defaultErr) 381 + } 382 + 383 + if string(spindles[0].Owner) != user.Did { 384 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 385 + s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 386 + return 387 + } 388 + 389 + member := r.FormValue("member") 390 + if member == "" { 391 + l.Error("empty member") 392 + s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 393 + return 394 + } 395 + l = l.With("member", member) 396 + 397 + memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 430 398 if err != nil { 431 - return "", err 399 + l.Error("failed to resolve member identity to handle", "err", err) 400 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 401 + return 402 + } 403 + if memberId.Handle.IsInvalidHandle() { 404 + l.Error("failed to resolve member identity to handle") 405 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 406 + return 432 407 } 433 408 434 - client := &http.Client{ 435 - Timeout: 1 * time.Second, 436 - } 437 - 438 - resp, err := client.Do(req.WithContext(ctx)) 439 - if err != nil || resp.StatusCode != 200 { 440 - return "", errors.New("failed to fetch /owner") 441 - } 442 - 443 - body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data 409 + // write to pds 410 + client, err := s.OAuth.AuthorizedClient(r) 444 411 if err != nil { 445 - return "", fmt.Errorf("failed to read /owner response: %w", err) 412 + l.Error("failed to authorize client", "err", err) 413 + fail() 414 + return 446 415 } 447 416 448 - did := strings.TrimSpace(string(body)) 449 - if did == "" { 450 - return "", errors.New("empty DID in /owner response") 417 + tx, err := s.Db.Begin() 418 + if err != nil { 419 + l.Error("failed to start txn", "err", err) 420 + fail() 421 + return 422 + } 423 + defer func() { 424 + tx.Rollback() 425 + s.Enforcer.E.LoadPolicy() 426 + }() 427 + 428 + rkey := appview.TID() 429 + 430 + // add member to db 431 + if err = db.AddSpindleMember(tx, db.SpindleMember{ 432 + Did: syntax.DID(user.Did), 433 + Rkey: rkey, 434 + Instance: instance, 435 + Subject: memberId.DID, 436 + }); err != nil { 437 + l.Error("failed to add spindle member", "err", err) 438 + fail() 439 + return 451 440 } 452 441 453 - return did, nil 442 + if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil { 443 + l.Error("failed to add member to ACLs") 444 + fail() 445 + return 446 + } 447 + 448 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 449 + Collection: tangled.SpindleMemberNSID, 450 + Repo: user.Did, 451 + Rkey: rkey, 452 + Record: &lexutil.LexiconTypeDecoder{ 453 + Val: &tangled.SpindleMember{ 454 + CreatedAt: time.Now().Format(time.RFC3339), 455 + Instance: instance, 456 + Subject: memberId.DID.String(), 457 + }, 458 + }, 459 + }) 460 + if err != nil { 461 + l.Error("failed to add record to PDS", "err", err) 462 + s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") 463 + return 464 + } 465 + 466 + if err = tx.Commit(); err != nil { 467 + l.Error("failed to commit txn", "err", err) 468 + fail() 469 + return 470 + } 471 + 472 + if err = s.Enforcer.E.SavePolicy(); err != nil { 473 + l.Error("failed to add member to ACLs", "err", err) 474 + fail() 475 + return 476 + } 477 + 478 + // success 479 + s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance)) 480 + } 481 + 482 + func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 483 + user := s.OAuth.GetUser(r) 484 + l := s.Logger.With("handler", "removeMember") 485 + 486 + noticeId := "operation-error" 487 + defaultErr := "Failed to add member. Try again later." 488 + fail := func() { 489 + s.Pages.Notice(w, noticeId, defaultErr) 490 + } 491 + 492 + instance := chi.URLParam(r, "instance") 493 + if instance == "" { 494 + l.Error("empty instance") 495 + fail() 496 + return 497 + } 498 + l = l.With("instance", instance) 499 + l = l.With("user", user.Did) 500 + 501 + spindles, err := db.GetSpindles( 502 + s.Db, 503 + db.FilterEq("owner", user.Did), 504 + db.FilterEq("instance", instance), 505 + ) 506 + if err != nil || len(spindles) != 1 { 507 + l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) 508 + fail() 509 + return 510 + } 511 + 512 + if string(spindles[0].Owner) != user.Did { 513 + l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 514 + s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 515 + return 516 + } 517 + 518 + member := r.FormValue("member") 519 + if member == "" { 520 + l.Error("empty member") 521 + s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") 522 + return 523 + } 524 + l = l.With("member", member) 525 + 526 + memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) 527 + if err != nil { 528 + l.Error("failed to resolve member identity to handle", "err", err) 529 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 530 + return 531 + } 532 + if memberId.Handle.IsInvalidHandle() { 533 + l.Error("failed to resolve member identity to handle") 534 + s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") 535 + return 536 + } 537 + 538 + tx, err := s.Db.Begin() 539 + if err != nil { 540 + l.Error("failed to start txn", "err", err) 541 + fail() 542 + return 543 + } 544 + defer func() { 545 + tx.Rollback() 546 + s.Enforcer.E.LoadPolicy() 547 + }() 548 + 549 + // get the record from the DB first: 550 + members, err := db.GetSpindleMembers( 551 + s.Db, 552 + db.FilterEq("did", user.Did), 553 + db.FilterEq("instance", instance), 554 + db.FilterEq("subject", memberId.DID), 555 + ) 556 + if err != nil || len(members) != 1 { 557 + l.Error("failed to get member", "err", err) 558 + fail() 559 + return 560 + } 561 + 562 + // remove from db 563 + if err = db.RemoveSpindleMember( 564 + tx, 565 + db.FilterEq("did", user.Did), 566 + db.FilterEq("instance", instance), 567 + db.FilterEq("subject", memberId.DID), 568 + ); err != nil { 569 + l.Error("failed to remove spindle member", "err", err) 570 + fail() 571 + return 572 + } 573 + 574 + // remove from enforcer 575 + if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil { 576 + l.Error("failed to update ACLs", "err", err) 577 + fail() 578 + return 579 + } 580 + 581 + client, err := s.OAuth.AuthorizedClient(r) 582 + if err != nil { 583 + l.Error("failed to authorize client", "err", err) 584 + fail() 585 + return 586 + } 587 + 588 + // remove from pds 589 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 590 + Collection: tangled.SpindleMemberNSID, 591 + Repo: user.Did, 592 + Rkey: members[0].Rkey, 593 + }) 594 + if err != nil { 595 + // non-fatal 596 + l.Error("failed to delete record", "err", err) 597 + } 598 + 599 + // commit everything 600 + if err = tx.Commit(); err != nil { 601 + l.Error("failed to commit txn", "err", err) 602 + fail() 603 + return 604 + } 605 + 606 + // commit everything 607 + if err = s.Enforcer.E.SavePolicy(); err != nil { 608 + l.Error("failed to save ACLs", "err", err) 609 + fail() 610 + return 611 + } 612 + 613 + // ok 614 + s.Pages.HxRefresh(w) 615 + return 454 616 }
+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 + }