From 8fe55f956ebe001471791875f1239a1556e97aab Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Thu, 22 Jan 2026 17:01:17 +0900 Subject: [PATCH] tmp: cmd/rbactester Signed-off-by: Seongmin Lee --- cmd/rbactester/main.go | 345 +++++++++++++++++++++++++++++++++++++++++ flake.nix | 1 + rbac2/knot.go | 29 ++++ rbac2/rbac2.go | 6 +- tap.env | 14 ++ 5 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 cmd/rbactester/main.go create mode 100644 rbac2/knot.go create mode 100644 tap.env diff --git a/cmd/rbactester/main.go b/cmd/rbactester/main.go new file mode 100644 index 00000000..a86fd96b --- /dev/null +++ b/cmd/rbactester/main.go @@ -0,0 +1,345 @@ +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 +} diff --git a/flake.nix b/flake.nix index 14beb990..bb2e7634 100644 --- a/flake.nix +++ b/flake.nix @@ -191,6 +191,7 @@ pkgs.coreutils # for those of us who are on systems that use busybox (alpine) packages'.lexgen packages'.treefmt-wrapper + packages'.tap ]; shellHook = '' mkdir -p appview/pages/static diff --git a/rbac2/knot.go b/rbac2/knot.go new file mode 100644 index 00000000..da5955b0 --- /dev/null +++ b/rbac2/knot.go @@ -0,0 +1,29 @@ +package rbac2 + +import "github.com/bluesky-social/indigo/atproto/syntax" + +func (e *Enforcer) SetKnotOwner(user syntax.DID, knot syntax.DID) error { + return e.setRoleForUser(user.String(), "server:owner", intoKnot(knot)) +} + +func (e *Enforcer) IsKnotMember(user syntax.DID, knot syntax.DID) (bool, error) { + return e.hasImplicitRoleForUser(user.String(), "server:member", intoKnot(knot)) +} + +func (e *Enforcer) AddKnotMember(user syntax.DID, knot syntax.DID) error { + _, err := e.e.AddRoleForUser(user.String(), "server:member", intoKnot(knot)) + return err +} + +func (e *Enforcer) RemoveKnotMember(user syntax.DID, knot syntax.DID) error { + _, err := e.e.DeleteRoleForUser(user.String(), "server:member", intoKnot(knot)) + return err +} + +func (e *Enforcer) IsKnotMemberInviteAllowed(user syntax.DID, knot syntax.DID) (bool, error) { + return e.e.Enforce(user.String(), intoKnot(knot), "/member", "write") +} + +func intoKnot(did syntax.DID) string { + return "/knot/" + did.String() +} diff --git a/rbac2/rbac2.go b/rbac2/rbac2.go index 4d42717c..8345e131 100644 --- a/rbac2/rbac2.go +++ b/rbac2/rbac2.go @@ -53,7 +53,7 @@ func NewEnforcerWithDB(db *sql.DB) (*Enforcer, error) { return nil, err } - a, err := adapter.NewAdapter(db, "sqlite3", "acl") + a, err := adapter.NewAdapter(db, "sqlite3", "acl2") if err != nil { return nil, err } @@ -95,6 +95,10 @@ func (e *Enforcer) CaptureModel() model.Model { return e.e.GetModel().Copy() } +func (e *Enforcer) Enforcer() *casbin.Enforcer { + return e.e +} + func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) { roles, err := e.e.GetImplicitRolesForUser(name, domain...) if err != nil { diff --git a/tap.env b/tap.env new file mode 100644 index 00000000..da515c1d --- /dev/null +++ b/tap.env @@ -0,0 +1,14 @@ +export TAP_BIND=:2481 +export TAP_RETRY_TIMEOUT=3s +export TAP_RESYNC_PARALLELISM=10 + +# export TAP_SIGNAL_COLLECTION=sh.tangled.repo.collaborator +export TAP_SIGNAL_COLLECTION=sh.tangled.repo + +export TAP_COLLECTION_FILTERS=sh.tangled.repo +export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.repo.collaborator + +# export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.knot +# export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.knot.member +# export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.spindle +# export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.spindle.member -- 2.51.2