package main import ( "context" "database/sql" "encoding/json" "fmt" "log/slog" "os" "strings" "github.com/bluesky-social/indigo/atproto/syntax" _ "github.com/mattn/go-sqlite3" "github.com/urfave/cli/v3" "tangled.org/core/api/tangled" "tangled.org/core/log" "tangled.org/core/rbac" "tangled.org/core/rbac2" "tangled.org/core/tap" ) func main() { cmd := &cli.Command{ Name: "rbactester", Usage: "test rbac2 package compatibility to legacy rbac package", Commands: []*cli.Command{ { Name: "backfill", Usage: "backfill rbac2", Action: backfill, Flags: []cli.Flag{ &cli.StringFlag{ Name: "db2", Usage: "db path for rbac2 package", Value: "rbac2.db", }, }, }, { Name: "test", Usage: "test rbac2 package", Action: test, Flags: []cli.Flag{ &cli.StringFlag{ Name: "db1", Usage: "original appview db path", Required: true, }, &cli.StringFlag{ Name: "db2", Usage: "db path for rbac2 package", Value: "rbac2.db", }, }, }, }, } logger := log.New("rbactester") slog.SetDefault(logger) ctx := context.Background() ctx = log.IntoContext(ctx, logger) if err := cmd.Run(ctx, os.Args); err != nil { logger.Error(err.Error()) os.Exit(-1) } } func backfill(ctx context.Context, cmd *cli.Command) error { l := log.FromContext(ctx) e2, err := rbac2.NewEnforcer(cmd.String("db2")) if err != nil { return fmt.Errorf("failed to initialize rbac enforcer: %w", err) } i := &Ingester{ e: e2, l: log.FromContext(ctx), } t := tap.NewClient("http://localhost:2481", "") l.Info("ingesting from tap") t.Connect(ctx, &tap.SimpleIndexer{ EventHandler: i.processEvent, }) return nil } func test(ctx context.Context, cmd *cli.Command) error { l := log.FromContext(ctx) e1, err := rbac.NewEnforcer(cmd.String("db1")) if err != nil { return fmt.Errorf("failed to initialize rbac enforcer: %w", err) } e2, err := rbac2.NewEnforcer(cmd.String("db2")) if err != nil { return fmt.Errorf("failed to initialize rbac enforcer: %w", err) } model := e2.CaptureModel() l.Info("debugging", "model", model) // check if boltless.me is collaborator of tangled.org/core // check, err := e2.IsRepoCollaborator(syntax.DID("did:plc:xasnlahkri4ewmbuzly2rlc5"), syntax.ATURI("at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22")) // l.Info("checking", "isCollab", check) policies, err := e2.Enforcer().GetGroupingPolicy() if err != nil { return fmt.Errorf("failed to get grouping policy: %w", err) } var users []syntax.DID for _, rule := range policies { sub := rule[0] if !strings.HasPrefix(sub, "did:") { l.Warn("no user", "sub", sub) continue // skip non-users (policy definitions) } users = append(users, syntax.DID(sub)) } repos, err := getRepos(cmd.String("db1")) if err != nil { return fmt.Errorf("failed to get repos: %w", err) } l.Info(fmt.Sprintf("testing over %d users with %d repos", len(users), len(repos))) for _, user := range users { for _, repo := range repos { // compare IsRepoCollaborator with two enforcer { check1, err := e1.IsRepoCollaborator(user.String(), repo.Knot, repo.DidSlashRepo()) assert(err) check2, err := e2.IsRepoCollaborator(user, repo.AtUri()) assert(err) if check1 == check2 { l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2) continue } l.Error("isCollaborator assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2) } // compare IsRepoOwner with two enforcer { check1, err := e1.IsRepoOwner(user.String(), repo.Knot, repo.DidSlashRepo()) assert(err) check2, err := e2.IsRepoOwner(user, repo.AtUri()) assert(err) if check1 == check2 { l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2) continue } l.Error("isOwner assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2) } } } return nil } type Repo struct { Did syntax.DID Rkey syntax.RecordKey Name string Knot string } func (r *Repo) DidSlashRepo() string { return fmt.Sprintf("%s/%s", r.Did, r.Rkey) } func (r *Repo) AtUri() syntax.ATURI { return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) } func getRepos(path string) ([]Repo, error) { db, err := sql.Open("sqlite3", path) if err != nil { return nil, err } rows, err := db.Query(`select did, rkey, name, knot from repos`) if err != nil { return nil, fmt.Errorf("failed to execute query: %w", err) } defer rows.Close() var repos []Repo for rows.Next() { var repo Repo if err := rows.Scan(&repo.Did, &repo.Rkey, &repo.Name, &repo.Knot); err != nil { return nil, fmt.Errorf("failed to execute repo query: %w", err) } repos = append(repos, repo) } return repos, nil } type Ingester struct { e *rbac2.Enforcer l *slog.Logger } func (i *Ingester) processEvent(ctx context.Context, evt tap.Event) error { var err error switch evt.Type { case tap.EvtRecord: i.l.Info("processing record", "live", evt.Record.Live, "action", evt.Record.Action, "at", evt.Record.AtUri()) switch evt.Record.Collection { case tangled.RepoNSID: err = i.processRepo(ctx, evt.Record) case tangled.RepoCollaboratorNSID: err = i.processRepoCollaborator(ctx, evt.Record) // case tangled.KnotNSID: // err = i.processKnot(ctx, evt.Record) // case tangled.KnotMemberNSID: // err = i.processKnotMember(ctx, evt.Record) // case tangled.SpindleNSID: // err = i.processSpindle(ctx, evt.Record) // case tangled.SpindleMemberNSID: // err = i.processSpindleMember(ctx, evt.Record) } } return err } func (i *Ingester) processRepo(_ context.Context, evt *tap.RecordEventData) error { switch evt.Action { case tap.RecordCreateAction, tap.RecordUpdateAction: record := tangled.Repo{} if err := json.Unmarshal(evt.Record, &record); err != nil { return fmt.Errorf("parsing record: %w", err) } assert(i.e.AddRepo(evt.AtUri())) case tap.RecordDeleteAction: i.l.Warn("skipping delete action", "at", evt.AtUri()) } return nil } func (i *Ingester) processRepoCollaborator(_ context.Context, evt *tap.RecordEventData) error { switch evt.Action { case tap.RecordCreateAction, tap.RecordUpdateAction: record := tangled.RepoCollaborator{} if err := json.Unmarshal(evt.Record, &record); err != nil { return fmt.Errorf("parsing record: %w", err) } ok, err := IsRepoCollaboratorInviteAllowed(evt.Did, syntax.ATURI(record.Repo)) if !ok || err != nil { i.l.Warn("forbidden request: collaborator invite not allowed", "at", evt.AtUri(), "error", err) return nil } assert(i.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo))) case tap.RecordDeleteAction: i.l.Warn("skipping delete action", "at", evt.AtUri()) } return nil } // func (i *Ingester) processKnot(_ context.Context, evt *tap.RecordEventData) error { // switch evt.Action { // case tap.RecordCreateAction, tap.RecordUpdateAction: // // assert(i.e.SetKnotOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey))) // // case tap.RecordDeleteAction: // i.l.Warn("skipping delete action", "at", evt.AtUri()) // } // return nil // } // // func (i *Ingester) processKnotMember(_ context.Context, evt *tap.RecordEventData) error { // switch evt.Action { // case tap.RecordCreateAction, tap.RecordUpdateAction: // record := tangled.KnotMember{} // if err := json.Unmarshal(evt.Record, &record); err != nil { // return fmt.Errorf("parsing record: %w", err) // } // ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Domain)) // if !ok || err != nil { // i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err) // return nil // } // // assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Domain))) // // case tap.RecordDeleteAction: // i.l.Warn("skipping delete action", "at", evt.AtUri()) // } // return nil // } // // func (i *Ingester) processSpindle(_ context.Context, evt *tap.RecordEventData) error { // switch evt.Action { // case tap.RecordCreateAction, tap.RecordUpdateAction: // // assert(i.e.SetSpindleOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey))) // // case tap.RecordDeleteAction: // i.l.Warn("skipping delete action", "at", evt.AtUri()) // } // return nil // } // // func (i *Ingester) processSpindleMember(_ context.Context, evt *tap.RecordEventData) error { // // switch evt.Action { // case tap.RecordCreateAction, tap.RecordUpdateAction: // record := tangled.SpindleMember{} // if err := json.Unmarshal(evt.Record, &record); err != nil { // return fmt.Errorf("parsing record: %w", err) // } // ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Instance)) // if !ok || err != nil { // i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err) // return nil // } // // assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Instance))) // // case tap.RecordDeleteAction: // i.l.Warn("skipping delete action", "at", evt.AtUri()) // } // return nil // } func assert(err error) { if err != nil { panic(err) } } // quickfix to perform ACL while ingesting func IsRepoCollaboratorInviteAllowed(did syntax.DID, repo syntax.ATURI) (bool, error) { return did.String() == repo.Authority().String(), nil }