···11+package main22+33+import (44+ "context"55+ "database/sql"66+ "encoding/json"77+ "fmt"88+ "log/slog"99+ "os"1010+ "strings"1111+1212+ "github.com/bluesky-social/indigo/atproto/syntax"1313+ _ "github.com/mattn/go-sqlite3"1414+ "github.com/urfave/cli/v3"1515+ "tangled.org/core/api/tangled"1616+ "tangled.org/core/log"1717+ "tangled.org/core/rbac"1818+ "tangled.org/core/rbac2"1919+ "tangled.org/core/tap"2020+)2121+2222+func main() {2323+ cmd := &cli.Command{2424+ Name: "rbactester",2525+ Usage: "test rbac2 package compatibility to legacy rbac package",2626+ Commands: []*cli.Command{2727+ {2828+ Name: "backfill",2929+ Usage: "backfill rbac2",3030+ Action: backfill,3131+ Flags: []cli.Flag{3232+ &cli.StringFlag{3333+ Name: "db2",3434+ Usage: "db path for rbac2 package",3535+ Value: "rbac2.db",3636+ },3737+ },3838+ },3939+ {4040+ Name: "test",4141+ Usage: "test rbac2 package",4242+ Action: test,4343+ Flags: []cli.Flag{4444+ &cli.StringFlag{4545+ Name: "db1",4646+ Usage: "original appview db path",4747+ Required: true,4848+ },4949+ &cli.StringFlag{5050+ Name: "db2",5151+ Usage: "db path for rbac2 package",5252+ Value: "rbac2.db",5353+ },5454+ },5555+ },5656+ },5757+ }5858+5959+ logger := log.New("rbactester")6060+ slog.SetDefault(logger)6161+6262+ ctx := context.Background()6363+ ctx = log.IntoContext(ctx, logger)6464+6565+ if err := cmd.Run(ctx, os.Args); err != nil {6666+ logger.Error(err.Error())6767+ os.Exit(-1)6868+ }6969+}7070+7171+func backfill(ctx context.Context, cmd *cli.Command) error {7272+ l := log.FromContext(ctx)7373+7474+ e2, err := rbac2.NewEnforcer(cmd.String("db2"))7575+ if err != nil {7676+ return fmt.Errorf("failed to initialize rbac enforcer: %w", err)7777+ }7878+7979+ i := &Ingester{8080+ e: e2,8181+ l: log.FromContext(ctx),8282+ }8383+8484+ t := tap.NewClient("http://localhost:2481", "")8585+ l.Info("ingesting from tap")8686+ t.Connect(ctx, &tap.SimpleIndexer{8787+ EventHandler: i.processEvent,8888+ })8989+9090+ return nil9191+}9292+9393+func test(ctx context.Context, cmd *cli.Command) error {9494+ l := log.FromContext(ctx)9595+9696+ e1, err := rbac.NewEnforcer(cmd.String("db1"))9797+ if err != nil {9898+ return fmt.Errorf("failed to initialize rbac enforcer: %w", err)9999+ }100100+101101+ e2, err := rbac2.NewEnforcer(cmd.String("db2"))102102+ if err != nil {103103+ return fmt.Errorf("failed to initialize rbac enforcer: %w", err)104104+ }105105+106106+ model := e2.CaptureModel()107107+ l.Info("debugging", "model", model)108108+109109+ // check if boltless.me is collaborator of tangled.org/core110110+ // check, err := e2.IsRepoCollaborator(syntax.DID("did:plc:xasnlahkri4ewmbuzly2rlc5"), syntax.ATURI("at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22"))111111+ // l.Info("checking", "isCollab", check)112112+113113+ policies, err := e2.Enforcer().GetGroupingPolicy()114114+ if err != nil {115115+ return fmt.Errorf("failed to get grouping policy: %w", err)116116+ }117117+ var users []syntax.DID118118+ for _, rule := range policies {119119+ sub := rule[0]120120+ if !strings.HasPrefix(sub, "did:") {121121+ l.Warn("no user", "sub", sub)122122+ continue // skip non-users (policy definitions)123123+ }124124+ users = append(users, syntax.DID(sub))125125+ }126126+127127+ repos, err := getRepos(cmd.String("db1"))128128+ if err != nil {129129+ return fmt.Errorf("failed to get repos: %w", err)130130+ }131131+132132+ l.Info(fmt.Sprintf("testing over %d users with %d repos", len(users), len(repos)))133133+ for _, user := range users {134134+ for _, repo := range repos {135135+ // compare IsRepoCollaborator with two enforcer136136+ {137137+ check1, err := e1.IsRepoCollaborator(user.String(), repo.Knot, repo.DidSlashRepo())138138+ assert(err)139139+ check2, err := e2.IsRepoCollaborator(user, repo.AtUri())140140+ assert(err)141141+ if check1 == check2 {142142+ l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2)143143+ continue144144+ }145145+ l.Error("isCollaborator assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2)146146+ }147147+ // compare IsRepoOwner with two enforcer148148+ {149149+ check1, err := e1.IsRepoOwner(user.String(), repo.Knot, repo.DidSlashRepo())150150+ assert(err)151151+ check2, err := e2.IsRepoOwner(user, repo.AtUri())152152+ assert(err)153153+ if check1 == check2 {154154+ l.Info("check succeed", "user", user, "repo", repo.AtUri(), "isCollab", check2)155155+ continue156156+ }157157+ l.Error("isOwner assertion failed", "user", user, "repo", repo.AtUri(), "c1", check1, "c2", check2)158158+ }159159+ }160160+ }161161+162162+ return nil163163+}164164+165165+type Repo struct {166166+ Did syntax.DID167167+ Rkey syntax.RecordKey168168+ Name string169169+ Knot string170170+}171171+172172+func (r *Repo) DidSlashRepo() string {173173+ return fmt.Sprintf("%s/%s", r.Did, r.Rkey)174174+}175175+176176+func (r *Repo) AtUri() syntax.ATURI {177177+ return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))178178+}179179+180180+func getRepos(path string) ([]Repo, error) {181181+ db, err := sql.Open("sqlite3", path)182182+ if err != nil {183183+ return nil, err184184+ }185185+ rows, err := db.Query(`select did, rkey, name, knot from repos`)186186+ if err != nil {187187+ return nil, fmt.Errorf("failed to execute query: %w", err)188188+ }189189+ defer rows.Close()190190+191191+ var repos []Repo192192+ for rows.Next() {193193+ var repo Repo194194+ if err := rows.Scan(&repo.Did, &repo.Rkey, &repo.Name, &repo.Knot); err != nil {195195+ return nil, fmt.Errorf("failed to execute repo query: %w", err)196196+ }197197+ repos = append(repos, repo)198198+ }199199+200200+ return repos, nil201201+}202202+203203+type Ingester struct {204204+ e *rbac2.Enforcer205205+ l *slog.Logger206206+}207207+208208+func (i *Ingester) processEvent(ctx context.Context, evt tap.Event) error {209209+ var err error210210+ switch evt.Type {211211+ case tap.EvtRecord:212212+ i.l.Info("processing record", "live", evt.Record.Live, "action", evt.Record.Action, "at", evt.Record.AtUri())213213+ switch evt.Record.Collection {214214+ case tangled.RepoNSID:215215+ err = i.processRepo(ctx, evt.Record)216216+ case tangled.RepoCollaboratorNSID:217217+ err = i.processRepoCollaborator(ctx, evt.Record)218218+ // case tangled.KnotNSID:219219+ // err = i.processKnot(ctx, evt.Record)220220+ // case tangled.KnotMemberNSID:221221+ // err = i.processKnotMember(ctx, evt.Record)222222+ // case tangled.SpindleNSID:223223+ // err = i.processSpindle(ctx, evt.Record)224224+ // case tangled.SpindleMemberNSID:225225+ // err = i.processSpindleMember(ctx, evt.Record)226226+ }227227+ }228228+ return err229229+}230230+231231+func (i *Ingester) processRepo(_ context.Context, evt *tap.RecordEventData) error {232232+ switch evt.Action {233233+ case tap.RecordCreateAction, tap.RecordUpdateAction:234234+ record := tangled.Repo{}235235+ if err := json.Unmarshal(evt.Record, &record); err != nil {236236+ return fmt.Errorf("parsing record: %w", err)237237+ }238238+239239+ assert(i.e.AddRepo(evt.AtUri()))240240+241241+ case tap.RecordDeleteAction:242242+ i.l.Warn("skipping delete action", "at", evt.AtUri())243243+ }244244+ return nil245245+}246246+247247+func (i *Ingester) processRepoCollaborator(_ context.Context, evt *tap.RecordEventData) error {248248+ switch evt.Action {249249+ case tap.RecordCreateAction, tap.RecordUpdateAction:250250+ record := tangled.RepoCollaborator{}251251+ if err := json.Unmarshal(evt.Record, &record); err != nil {252252+ return fmt.Errorf("parsing record: %w", err)253253+ }254254+255255+ ok, err := IsRepoCollaboratorInviteAllowed(evt.Did, syntax.ATURI(record.Repo))256256+ if !ok || err != nil {257257+ i.l.Warn("forbidden request: collaborator invite not allowed", "at", evt.AtUri(), "error", err)258258+ return nil259259+ }260260+261261+ assert(i.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)))262262+263263+ case tap.RecordDeleteAction:264264+ i.l.Warn("skipping delete action", "at", evt.AtUri())265265+ }266266+ return nil267267+}268268+269269+// func (i *Ingester) processKnot(_ context.Context, evt *tap.RecordEventData) error {270270+// switch evt.Action {271271+// case tap.RecordCreateAction, tap.RecordUpdateAction:272272+//273273+// assert(i.e.SetKnotOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey)))274274+//275275+// case tap.RecordDeleteAction:276276+// i.l.Warn("skipping delete action", "at", evt.AtUri())277277+// }278278+// return nil279279+// }280280+//281281+// func (i *Ingester) processKnotMember(_ context.Context, evt *tap.RecordEventData) error {282282+// switch evt.Action {283283+// case tap.RecordCreateAction, tap.RecordUpdateAction:284284+// record := tangled.KnotMember{}285285+// if err := json.Unmarshal(evt.Record, &record); err != nil {286286+// return fmt.Errorf("parsing record: %w", err)287287+// }288288+// ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Domain))289289+// if !ok || err != nil {290290+// i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err)291291+// return nil292292+// }293293+//294294+// assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Domain)))295295+//296296+// case tap.RecordDeleteAction:297297+// i.l.Warn("skipping delete action", "at", evt.AtUri())298298+// }299299+// return nil300300+// }301301+//302302+// func (i *Ingester) processSpindle(_ context.Context, evt *tap.RecordEventData) error {303303+// switch evt.Action {304304+// case tap.RecordCreateAction, tap.RecordUpdateAction:305305+//306306+// assert(i.e.SetSpindleOwner(evt.Did, syntax.DID("did:web:"+evt.Rkey)))307307+//308308+// case tap.RecordDeleteAction:309309+// i.l.Warn("skipping delete action", "at", evt.AtUri())310310+// }311311+// return nil312312+// }313313+//314314+// func (i *Ingester) processSpindleMember(_ context.Context, evt *tap.RecordEventData) error {315315+//316316+// switch evt.Action {317317+// case tap.RecordCreateAction, tap.RecordUpdateAction:318318+// record := tangled.SpindleMember{}319319+// if err := json.Unmarshal(evt.Record, &record); err != nil {320320+// return fmt.Errorf("parsing record: %w", err)321321+// }322322+// ok, err := i.e.IsSpindleMemberInviteAllowed(evt.Did, syntax.DID("did:web:"+record.Instance))323323+// if !ok || err != nil {324324+// i.l.Warn("forbidden request: member invite not allowed", "at", evt.AtUri(), "error", err)325325+// return nil326326+// }327327+//328328+// assert(i.e.AddSpindleMember(syntax.DID(record.Subject), syntax.DID("did:web:"+record.Instance)))329329+//330330+// case tap.RecordDeleteAction:331331+// i.l.Warn("skipping delete action", "at", evt.AtUri())332332+// }333333+// return nil334334+// }335335+336336+func assert(err error) {337337+ if err != nil {338338+ panic(err)339339+ }340340+}341341+342342+// quickfix to perform ACL while ingesting343343+func IsRepoCollaboratorInviteAllowed(did syntax.DID, repo syntax.ATURI) (bool, error) {344344+ return did.String() == repo.Authority().String(), nil345345+}
+1
flake.nix
···191191 pkgs.coreutils # for those of us who are on systems that use busybox (alpine)192192 packages'.lexgen193193 packages'.treefmt-wrapper194194+ packages'.tap194195 ];195196 shellHook = ''196197 mkdir -p appview/pages/static