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