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

appview: implement new ownership process for knots

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

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

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

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