1package serververify
2
3import (
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
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 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
55type OwnerMismatch struct {
56 expected string
57 observed string
58}
59
60func (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
65func 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
82func 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
121func 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.Register(tx, domain)
133 if err != nil {
134 return fmt.Errorf("failed to register domain: %w", err)
135 }
136
137 // add basic acls for this domain
138 err = e.AddKnot(domain)
139 if err != nil {
140 return fmt.Errorf("failed to add knot to enforcer: %w", err)
141 }
142
143 // add this did as owner of this domain
144 err = e.AddKnotOwner(domain, owner)
145 if err != nil {
146 return fmt.Errorf("failed to add knot owner to enforcer: %w", err)
147 }
148
149 err = tx.Commit()
150 if err != nil {
151 return fmt.Errorf("failed to commit changes: %w", err)
152 }
153
154 err = e.E.SavePolicy()
155 if err != nil {
156 return fmt.Errorf("failed to update ACLs: %w", err)
157 }
158
159 return nil
160}