Monorepo for Tangled
tangled.org
1package serververify
2
3import (
4 "context"
5 "errors"
6 "fmt"
7
8 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
9 "tangled.org/core/api/tangled"
10 "tangled.org/core/appview/db"
11 "tangled.org/core/appview/xrpcclient"
12 "tangled.org/core/orm"
13 "tangled.org/core/rbac"
14)
15
16var (
17 FetchError = errors.New("failed to fetch owner")
18)
19
20// fetchOwner fetches the owner DID from a server's /owner endpoint
21func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) {
22 scheme := "https"
23 if dev {
24 scheme = "http"
25 }
26
27 host := fmt.Sprintf("%s://%s", scheme, domain)
28 xrpcc := &indigoxrpc.Client{
29 Host: host,
30 }
31
32 res, err := tangled.Owner(ctx, xrpcc)
33 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
34 return "", xrpcerr
35 }
36
37 return res.Owner, nil
38}
39
40type OwnerMismatch struct {
41 expected string
42 observed string
43}
44
45func (e *OwnerMismatch) Error() string {
46 return fmt.Sprintf("owner mismatch: %q != %q", e.expected, e.observed)
47}
48
49// RunVerification verifies that the server at the given domain has the expected owner
50func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool) error {
51 observedOwner, err := fetchOwner(ctx, domain, dev)
52 if err != nil {
53 return err
54 }
55
56 if observedOwner != expectedOwner {
57 return &OwnerMismatch{
58 expected: expectedOwner,
59 observed: observedOwner,
60 }
61 }
62
63 return nil
64}
65
66// MarkSpindleVerified marks a spindle as verified in the DB and adds the user as its owner
67func MarkSpindleVerified(d *db.DB, e *rbac.Enforcer, instance, owner string) (int64, error) {
68 tx, err := d.Begin()
69 if err != nil {
70 return 0, fmt.Errorf("failed to create txn: %w", err)
71 }
72 committed := false
73 defer func() {
74 if committed {
75 return
76 }
77 tx.Rollback()
78 e.E.LoadPolicy()
79 }()
80
81 // mark this spindle as verified in the db
82 rowId, err := db.VerifySpindle(
83 tx,
84 orm.FilterEq("owner", owner),
85 orm.FilterEq("instance", instance),
86 )
87 if err != nil {
88 return 0, fmt.Errorf("failed to write to DB: %w", err)
89 }
90
91 err = e.AddSpindleOwner(instance, owner)
92 if err != nil {
93 return 0, fmt.Errorf("failed to update ACL: %w", err)
94 }
95
96 err = tx.Commit()
97 if err != nil {
98 return 0, fmt.Errorf("failed to commit txn: %w", err)
99 }
100
101 err = e.E.SavePolicy()
102 if err != nil {
103 return 0, fmt.Errorf("failed to update ACL: %w", err)
104 }
105 committed = true
106
107 return rowId, nil
108}
109
110// MarkKnotVerified marks a knot as verified and sets up ownership/permissions
111func MarkKnotVerified(d *db.DB, e *rbac.Enforcer, domain, owner string) error {
112 tx, err := d.BeginTx(context.Background(), nil)
113 if err != nil {
114 return fmt.Errorf("failed to start tx: %w", err)
115 }
116 committed := false
117 defer func() {
118 if committed {
119 return
120 }
121 tx.Rollback()
122 e.E.LoadPolicy()
123 }()
124
125 // mark as registered
126 err = db.MarkRegistered(
127 tx,
128 orm.FilterEq("did", owner),
129 orm.FilterEq("domain", domain),
130 )
131 if err != nil {
132 return fmt.Errorf("failed to register domain: %w", err)
133 }
134
135 // add basic acls for this domain
136 err = e.AddKnot(domain)
137 if err != nil {
138 return fmt.Errorf("failed to add knot to enforcer: %w", err)
139 }
140
141 // add this did as owner of this domain
142 err = e.AddKnotOwner(domain, owner)
143 if err != nil {
144 return fmt.Errorf("failed to add knot owner to enforcer: %w", err)
145 }
146
147 err = tx.Commit()
148 if err != nil {
149 return fmt.Errorf("failed to commit changes: %w", err)
150 }
151
152 err = e.E.SavePolicy()
153 if err != nil {
154 return fmt.Errorf("failed to update ACLs: %w", err)
155 }
156 committed = true
157
158 return nil
159}