tmp: cmd/rbactester

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me bc9d8301 11e240ff

verified
+394 -1
+345
cmd/rbactester/main.go
···
··· 1 + package main 2 + 3 + import ( 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 + 22 + func 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 + 71 + func 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 + 93 + func 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 + 165 + type Repo struct { 166 + Did syntax.DID 167 + Rkey syntax.RecordKey 168 + Name string 169 + Knot string 170 + } 171 + 172 + func (r *Repo) DidSlashRepo() string { 173 + return fmt.Sprintf("%s/%s", r.Did, r.Rkey) 174 + } 175 + 176 + func (r *Repo) AtUri() syntax.ATURI { 177 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 178 + } 179 + 180 + func 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 + 203 + type Ingester struct { 204 + e *rbac2.Enforcer 205 + l *slog.Logger 206 + } 207 + 208 + func (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 + 231 + func (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 + 247 + func (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 + 336 + func assert(err error) { 337 + if err != nil { 338 + panic(err) 339 + } 340 + } 341 + 342 + // quickfix to perform ACL while ingesting 343 + func IsRepoCollaboratorInviteAllowed(did syntax.DID, repo syntax.ATURI) (bool, error) { 344 + return did.String() == repo.Authority().String(), nil 345 + }
+1
flake.nix
··· 191 pkgs.coreutils # for those of us who are on systems that use busybox (alpine) 192 packages'.lexgen 193 packages'.treefmt-wrapper 194 ]; 195 shellHook = '' 196 mkdir -p appview/pages/static
··· 191 pkgs.coreutils # for those of us who are on systems that use busybox (alpine) 192 packages'.lexgen 193 packages'.treefmt-wrapper 194 + packages'.tap 195 ]; 196 shellHook = '' 197 mkdir -p appview/pages/static
+29
rbac2/knot.go
···
··· 1 + package rbac2 2 + 3 + import "github.com/bluesky-social/indigo/atproto/syntax" 4 + 5 + func (e *Enforcer) SetKnotOwner(user syntax.DID, knot syntax.DID) error { 6 + return e.setRoleForUser(user.String(), "server:owner", intoKnot(knot)) 7 + } 8 + 9 + func (e *Enforcer) IsKnotMember(user syntax.DID, knot syntax.DID) (bool, error) { 10 + return e.hasImplicitRoleForUser(user.String(), "server:member", intoKnot(knot)) 11 + } 12 + 13 + func (e *Enforcer) AddKnotMember(user syntax.DID, knot syntax.DID) error { 14 + _, err := e.e.AddRoleForUser(user.String(), "server:member", intoKnot(knot)) 15 + return err 16 + } 17 + 18 + func (e *Enforcer) RemoveKnotMember(user syntax.DID, knot syntax.DID) error { 19 + _, err := e.e.DeleteRoleForUser(user.String(), "server:member", intoKnot(knot)) 20 + return err 21 + } 22 + 23 + func (e *Enforcer) IsKnotMemberInviteAllowed(user syntax.DID, knot syntax.DID) (bool, error) { 24 + return e.e.Enforce(user.String(), intoKnot(knot), "/member", "write") 25 + } 26 + 27 + func intoKnot(did syntax.DID) string { 28 + return "/knot/" + did.String() 29 + }
+5 -1
rbac2/rbac2.go
··· 53 return nil, err 54 } 55 56 - a, err := adapter.NewAdapter(db, "sqlite3", "acl") 57 if err != nil { 58 return nil, err 59 } ··· 93 // CaptureModel returns copy of current model. Used for testing 94 func (e *Enforcer) CaptureModel() model.Model { 95 return e.e.GetModel().Copy() 96 } 97 98 func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
··· 53 return nil, err 54 } 55 56 + a, err := adapter.NewAdapter(db, "sqlite3", "acl2") 57 if err != nil { 58 return nil, err 59 } ··· 93 // CaptureModel returns copy of current model. Used for testing 94 func (e *Enforcer) CaptureModel() model.Model { 95 return e.e.GetModel().Copy() 96 + } 97 + 98 + func (e *Enforcer) Enforcer() *casbin.Enforcer { 99 + return e.e 100 } 101 102 func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
+14
tap.env
···
··· 1 + export TAP_BIND=:2481 2 + export TAP_RETRY_TIMEOUT=3s 3 + export TAP_RESYNC_PARALLELISM=10 4 + 5 + # export TAP_SIGNAL_COLLECTION=sh.tangled.repo.collaborator 6 + export TAP_SIGNAL_COLLECTION=sh.tangled.repo 7 + 8 + export TAP_COLLECTION_FILTERS=sh.tangled.repo 9 + export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.repo.collaborator 10 + 11 + # export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.knot 12 + # export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.knot.member 13 + # export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.spindle 14 + # export TAP_COLLECTION_FILTERS=$TAP_COLLECTION_FILTERS,sh.tangled.spindle.member