Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "context"
5 "database/sql"
6 "log/slog"
7 "net/http"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.org/core/knotserver/db"
11 "tangled.org/core/knotserver/keys"
12 xrpcerr "tangled.org/core/xrpc/errors"
13)
14
15type aclGrant struct {
16 role string
17 subject syntax.DID
18 inAcl func() (bool, error)
19 inTable func() (bool, error)
20 insertRow func(*sql.Tx) error
21 deleteRow func() error
22 grantAcl func() error
23 emit func() error
24}
25
26type aclRevoke struct {
27 role string
28 subject syntax.DID
29 inAcl func() (bool, error)
30 inTable func() (bool, error)
31 removeAcl func() (bool, error)
32 restoreAcl func() error
33 deleteRow func(*sql.Tx) error
34 emit func() error
35}
36
37func (h *Xrpc) applyAclGrant(ctx context.Context, l *slog.Logger, g aclGrant) (int, *xrpcerr.XrpcError) {
38 fail := func(status int, e xrpcerr.XrpcError) (int, *xrpcerr.XrpcError) {
39 return status, &e
40 }
41
42 inAcl, err := g.inAcl()
43 if err != nil {
44 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
45 }
46 inTable, err := g.inTable()
47 if err != nil {
48 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
49 }
50 if inAcl && inTable {
51 l.Info("subject already granted, no-op", "role", g.role, "subject", g.subject)
52 return http.StatusOK, nil
53 }
54
55 didKnown, err := db.IsDidKnown(h.Db, g.subject.String())
56 if err != nil {
57 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
58 }
59
60 tx, err := h.Db.BeginTx(ctx, nil)
61 if err != nil {
62 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
63 }
64 committed := false
65 defer func() {
66 if !committed {
67 tx.Rollback()
68 }
69 }()
70
71 if err := db.AddDid(tx, g.subject.String()); err != nil {
72 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
73 }
74 if err := g.insertRow(tx); err != nil {
75 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
76 }
77 if err := tx.Commit(); err != nil {
78 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
79 }
80 committed = true
81
82 if err := g.grantAcl(); err != nil {
83 if !inTable {
84 if rbErr := g.deleteRow(); rbErr != nil {
85 l.Error("failed to roll back row after ACL grant failed", "role", g.role, "subject", g.subject, "error", rbErr)
86 }
87 }
88 if !didKnown {
89 if rbErr := db.RemoveDid(h.Db, g.subject.String()); rbErr != nil {
90 l.Error("failed to roll back known_did after ACL grant failed", "role", g.role, "subject", g.subject, "error", rbErr)
91 }
92 }
93 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
94 }
95
96 h.Ingester.AddDid(g.subject.String())
97 h.fetchKeysAsync(ctx, l, g.subject)
98
99 if g.emit != nil {
100 if err := g.emit(); err != nil {
101 l.Error("failed to emit acl grant event, appview reconcile will catch up", "role", g.role, "subject", g.subject, "error", err)
102 }
103 }
104
105 l.Info("granted", "role", g.role, "subject", g.subject)
106 return http.StatusOK, nil
107}
108
109func (h *Xrpc) applyAclRevoke(ctx context.Context, l *slog.Logger, rv aclRevoke) (int, *xrpcerr.XrpcError) {
110 fail := func(status int, e xrpcerr.XrpcError) (int, *xrpcerr.XrpcError) {
111 return status, &e
112 }
113
114 inAcl, err := rv.inAcl()
115 if err != nil {
116 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
117 }
118 inTable, err := rv.inTable()
119 if err != nil {
120 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
121 }
122 if !inAcl && !inTable {
123 l.Info("subject not granted, no-op", "role", rv.role, "subject", rv.subject)
124 return http.StatusOK, nil
125 }
126
127 removed := false
128 if inAcl {
129 removed, err = rv.removeAcl()
130 if err != nil {
131 return fail(http.StatusInternalServerError, xrpcerr.GenericError(err))
132 }
133 }
134
135 failRestoringACL := func(status int, e xrpcerr.XrpcError) (int, *xrpcerr.XrpcError) {
136 if removed {
137 if rbErr := rv.restoreAcl(); rbErr != nil {
138 l.Error("failed to restore ACL after remove rollback", "role", rv.role, "subject", rv.subject, "error", rbErr)
139 }
140 }
141 return fail(status, e)
142 }
143
144 stillKnown, err := h.Enforcer.HasAnyPolicyForUser(rv.subject.String())
145 if err != nil {
146 return failRestoringACL(http.StatusInternalServerError, xrpcerr.GenericError(err))
147 }
148
149 tx, err := h.Db.BeginTx(ctx, nil)
150 if err != nil {
151 return failRestoringACL(http.StatusInternalServerError, xrpcerr.GenericError(err))
152 }
153 committed := false
154 defer func() {
155 if !committed {
156 tx.Rollback()
157 }
158 }()
159
160 if err := rv.deleteRow(tx); err != nil {
161 return failRestoringACL(http.StatusInternalServerError, xrpcerr.GenericError(err))
162 }
163 if !stillKnown {
164 if err := db.RemoveDid(tx, rv.subject.String()); err != nil {
165 return failRestoringACL(http.StatusInternalServerError, xrpcerr.GenericError(err))
166 }
167 }
168 if err := tx.Commit(); err != nil {
169 return failRestoringACL(http.StatusInternalServerError, xrpcerr.GenericError(err))
170 }
171 committed = true
172
173 if !stillKnown {
174 h.Ingester.RemoveDid(rv.subject.String())
175 }
176
177 if rv.emit != nil {
178 if err := rv.emit(); err != nil {
179 l.Error("failed to emit acl revoke event, appview reconcile will catch up", "role", rv.role, "subject", rv.subject, "error", err)
180 }
181 }
182
183 l.Info("revoked", "role", rv.role, "subject", rv.subject, "did_dropped", !stillKnown)
184 return http.StatusOK, nil
185}
186
187func (h *Xrpc) fetchKeysAsync(ctx context.Context, l *slog.Logger, subject syntax.DID) {
188 kctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), keyFetchTimeout)
189 go func() {
190 defer cancel()
191 if err := keys.FetchAndStore(kctx, h.Resolver.Directory(), h.Db, subject); err != nil {
192 l.Warn("failed to fetch subject public keys, continuing", "subject", subject, "error", err)
193 }
194 }()
195}