forked from
tangled.org/core
Monorepo for Tangled
1package main
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "fmt"
8 "log/slog"
9 "os"
10 "strings"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 _ "github.com/mattn/go-sqlite3"
14 "github.com/urfave/cli/v3"
15 "tangled.org/core/api/tangled"
16 "tangled.org/core/log"
17 "tangled.org/core/rbac"
18 "tangled.org/core/rbac2"
19 "tangled.org/core/tap"
20)
21
22func main() {
23 cmd := &cli.Command{
24 Name: "rbactester",
25 Usage: "test rbac2 package compatibility to legacy rbac package",
26 Commands: []*cli.Command{
27 {
28 Name: "backfill",
29 Usage: "backfill rbac2",
30 Action: backfill,
31 Flags: []cli.Flag{
32 &cli.StringFlag{
33 Name: "db2",
34 Usage: "db path for rbac2 package",
35 Value: "rbac2.db",
36 },
37 },
38 },
39 {
40 Name: "test",
41 Usage: "test rbac2 package",
42 Action: test,
43 Flags: []cli.Flag{
44 &cli.StringFlag{
45 Name: "db1",
46 Usage: "original appview db path",
47 Required: true,
48 },
49 &cli.StringFlag{
50 Name: "db2",
51 Usage: "db path for rbac2 package",
52 Value: "rbac2.db",
53 },
54 },
55 },
56 },
57 }
58
59 logger := log.New("rbactester")
60 slog.SetDefault(logger)
61
62 ctx := context.Background()
63 ctx = log.IntoContext(ctx, logger)
64
65 if err := cmd.Run(ctx, os.Args); err != nil {
66 logger.Error(err.Error())
67 os.Exit(-1)
68 }
69}
70
71func backfill(ctx context.Context, cmd *cli.Command) error {
72 l := log.FromContext(ctx)
73
74 e2, err := rbac2.NewEnforcer(cmd.String("db2"))
75 if err != nil {
76 return fmt.Errorf("failed to initialize rbac enforcer: %w", err)
77 }
78
79 i := &Ingester{
80 e: e2,
81 l: log.FromContext(ctx),
82 }
83
84 t := tap.NewClient("http://localhost:2481", "")
85 l.Info("ingesting from tap")
86 t.Connect(ctx, &tap.SimpleIndexer{
87 EventHandler: i.processEvent,
88 })
89
90 return nil
91}
92
93func test(ctx context.Context, cmd *cli.Command) error {
94 l := log.FromContext(ctx)
95
96 e1, err := rbac.NewEnforcer(cmd.String("db1"))
97 if err != nil {
98 return fmt.Errorf("failed to initialize rbac enforcer: %w", err)
99 }
100
101 e2, err := rbac2.NewEnforcer(cmd.String("db2"))
102 if err != nil {
103 return fmt.Errorf("failed to initialize rbac enforcer: %w", err)
104 }
105
106 model := e2.CaptureModel()
107 l.Info("debugging", "model", model)
108
109 // check if boltless.me is collaborator of tangled.org/core
110 // check, err := e2.IsRepoCollaborator(syntax.DID("did:plc:xasnlahkri4ewmbuzly2rlc5"), syntax.ATURI("at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22"))
111 // l.Info("checking", "isCollab", check)
112
113 policies, err := e2.Enforcer().GetGroupingPolicy()
114 if err != nil {
115 return fmt.Errorf("failed to get grouping policy: %w", err)
116 }
117 var users []syntax.DID
118 for _, rule := range policies {
119 sub := rule[0]
120 if !strings.HasPrefix(sub, "did:") {
121 l.Warn("no user", "sub", sub)
122 continue // skip non-users (policy definitions)
123 }
124 users = append(users, syntax.DID(sub))
125 }
126
127 repos, err := getRepos(cmd.String("db1"))
128 if err != nil {
129 return fmt.Errorf("failed to get repos: %w", err)
130 }
131
132 l.Info(fmt.Sprintf("testing over %d users with %d repos", len(users), len(repos)))
133 for _, user := range users {
134 for _, repo := range repos {
135 // compare IsRepoCollaborator with two enforcer
136 {
137 check1, err := e1.IsRepoCollaborator(user.String(), repo.Knot, repo.DidSlashRepo())
138 assert(err)
139 check2, err := e2.IsRepoCollaborator(user, repo.AtUri())
140 assert(err)
141 if check1 == check2 {
142 l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2)
143 continue
144 }
145 l.Error("isCollaborator assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2)
146 }
147 // compare IsRepoOwner with two enforcer
148 {
149 check1, err := e1.IsRepoOwner(user.String(), repo.Knot, repo.DidSlashRepo())
150 assert(err)
151 check2, err := e2.IsRepoOwner(user, repo.AtUri())
152 assert(err)
153 if check1 == check2 {
154 l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2)
155 continue
156 }
157 l.Error("isOwner assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2)
158 }
159 }
160 }
161
162 return nil
163}
164
165type Repo struct {
166 Did syntax.DID
167 Rkey syntax.RecordKey
168 Name string
169 Knot string
170}
171
172func (r *Repo) DidSlashRepo() string {
173 return fmt.Sprintf("%s/%s", r.Did, r.Rkey)
174}
175
176func (r *Repo) AtUri() syntax.ATURI {
177 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
178}
179
180func getRepos(path string) ([]Repo, error) {
181 db, err := sql.Open("sqlite3", path)
182 if err != nil {
183 return nil, err
184 }
185 rows, err := db.Query(`select did, rkey, name, knot from repos`)
186 if err != nil {
187 return nil, fmt.Errorf("failed to execute query: %w", err)
188 }
189 defer rows.Close()
190
191 var repos []Repo
192 for rows.Next() {
193 var repo Repo
194 if err := rows.Scan(&repo.Did, &repo.Rkey, &repo.Name, &repo.Knot); err != nil {
195 return nil, fmt.Errorf("failed to execute repo query: %w", err)
196 }
197 repos = append(repos, repo)
198 }
199
200 return repos, nil
201}
202
203type Ingester struct {
204 e *rbac2.Enforcer
205 l *slog.Logger
206}
207
208func (i *Ingester) processEvent(ctx context.Context, evt tap.Event) error {
209 var err error
210 switch evt.Type {
211 case tap.EvtRecord:
212 i.l.Info("processing record", "live", evt.Record.Live, "action", evt.Record.Action, "at", evt.Record.AtUri())
213 switch evt.Record.Collection {
214 case tangled.RepoNSID:
215 err = i.processRepo(ctx, evt.Record)
216 case tangled.RepoCollaboratorNSID:
217 err = i.processRepoCollaborator(ctx, evt.Record)
218 // case tangled.KnotNSID:
219 // err = i.processKnot(ctx, evt.Record)
220 // case tangled.KnotMemberNSID:
221 // err = i.processKnotMember(ctx, evt.Record)
222 // case tangled.SpindleNSID:
223 // err = i.processSpindle(ctx, evt.Record)
224 // case tangled.SpindleMemberNSID:
225 // err = i.processSpindleMember(ctx, evt.Record)
226 }
227 }
228 return err
229}
230
231func (i *Ingester) processRepo(_ context.Context, evt *tap.RecordEventData) error {
232 switch evt.Action {
233 case tap.RecordCreateAction, tap.RecordUpdateAction:
234 record := tangled.Repo{}
235 if err := json.Unmarshal(evt.Record, &record); err != nil {
236 return fmt.Errorf("parsing record: %w", err)
237 }
238
239 assert(i.e.AddRepo(evt.AtUri()))
240
241 case tap.RecordDeleteAction:
242 i.l.Warn("skipping delete action", "at", evt.AtUri())
243 }
244 return nil
245}
246
247func (i *Ingester) processRepoCollaborator(_ context.Context, evt *tap.RecordEventData) error {
248 switch evt.Action {
249 case tap.RecordCreateAction, tap.RecordUpdateAction:
250 record := tangled.RepoCollaborator{}
251 if err := json.Unmarshal(evt.Record, &record); err != nil {
252 return fmt.Errorf("parsing record: %w", err)
253 }
254
255 ok, err := IsRepoCollaboratorInviteAllowed(evt.Did, syntax.ATURI(record.Repo))
256 if !ok || err != nil {
257 i.l.Warn("forbidden request: collaborator invite not allowed", "at", evt.AtUri(), "error", err)
258 return nil
259 }
260
261 assert(i.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)))
262
263 case tap.RecordDeleteAction:
264 i.l.Warn("skipping delete action", "at", evt.AtUri())
265 }
266 return nil
267}
268
269// func (i *Ingester) processKnot(_ context.Context, evt *tap.RecordEventData) error {
270// switch evt.Action {
271// case tap.RecordCreateAction, tap.RecordUpdateAction:
272//
273// assert(i.e.SetKnotOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey)))
274//
275// case tap.RecordDeleteAction:
276// i.l.Warn("skipping delete action", "at", evt.AtUri())
277// }
278// return nil
279// }
280//
281// func (i *Ingester) processKnotMember(_ context.Context, evt *tap.RecordEventData) error {
282// switch evt.Action {
283// case tap.RecordCreateAction, tap.RecordUpdateAction:
284// record := tangled.KnotMember{}
285// if err := json.Unmarshal(evt.Record, &record); err != nil {
286// return fmt.Errorf("parsing record: %w", err)
287// }
288// ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Domain))
289// if !ok || err != nil {
290// i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err)
291// return nil
292// }
293//
294// assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Domain)))
295//
296// case tap.RecordDeleteAction:
297// i.l.Warn("skipping delete action", "at", evt.AtUri())
298// }
299// return nil
300// }
301//
302// func (i *Ingester) processSpindle(_ context.Context, evt *tap.RecordEventData) error {
303// switch evt.Action {
304// case tap.RecordCreateAction, tap.RecordUpdateAction:
305//
306// assert(i.e.SetSpindleOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey)))
307//
308// case tap.RecordDeleteAction:
309// i.l.Warn("skipping delete action", "at", evt.AtUri())
310// }
311// return nil
312// }
313//
314// func (i *Ingester) processSpindleMember(_ context.Context, evt *tap.RecordEventData) error {
315//
316// switch evt.Action {
317// case tap.RecordCreateAction, tap.RecordUpdateAction:
318// record := tangled.SpindleMember{}
319// if err := json.Unmarshal(evt.Record, &record); err != nil {
320// return fmt.Errorf("parsing record: %w", err)
321// }
322// ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Instance))
323// if !ok || err != nil {
324// i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err)
325// return nil
326// }
327//
328// assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Instance)))
329//
330// case tap.RecordDeleteAction:
331// i.l.Warn("skipping delete action", "at", evt.AtUri())
332// }
333// return nil
334// }
335
336func assert(err error) {
337 if err != nil {
338 panic(err)
339 }
340}
341
342// quickfix to perform ACL while ingesting
343func IsRepoCollaboratorInviteAllowed(did syntax.DID, repo syntax.ATURI) (bool, error) {
344 return did.String() == repo.Authority().String(), nil
345}