-176
appview/spindleresolver/resolver.go
-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
+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
+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
+
}