Monorepo for Tangled
at sl/rbac2test 345 lines 9.5 kB view raw
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}