package rbac2 import ( "database/sql" "fmt" adapter "github.com/Blank-Xu/sql-adapter" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/util" "tangled.org/core/api/tangled" ) const ( Model = ` [request_definition] r = sub, dom, obj, act [policy_definition] p = sub, dom, obj, act [role_definition] g = _, _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub, r.dom) && keyMatch4(r.dom, p.dom) && r.obj == p.obj && r.act == p.act ` ) type Enforcer struct { e *casbin.Enforcer } func NewEnforcer(path string) (*Enforcer, error) { m, err := model.NewModelFromString(Model) if err != nil { return nil, err } db, err := sql.Open("sqlite3", path+"?_foreign_keys=1") if err != nil { return nil, err } a, err := adapter.NewAdapter(db, "sqlite3", "acl") if err != nil { return nil, err } e, err := casbin.NewEnforcer(m, a) if err != nil { return nil, err } if err := seedTangledPolicies(e); err != nil { return nil, err } return &Enforcer{e}, nil } func seedTangledPolicies(e *casbin.Enforcer) error { // policies aturi := func(nsid string) string { return fmt.Sprintf("at://{did}/%s/{rkey}", nsid) } _, err := e.AddPoliciesEx([][]string{ // sub | dom | obj | act {"repo:owner", aturi(tangled.RepoNSID), "/", "write"}, {"repo:owner", aturi(tangled.RepoNSID), "/collaborator", "write"}, // invite {"repo:collaborator", aturi(tangled.RepoNSID), "/settings", "write"}, {"repo:collaborator", aturi(tangled.RepoNSID), "/git", "write"}, // git push {"server:owner", "/knot/{did}", "/member", "write"}, // invite {"server:member", "/knot/{did}", "/git", "write"}, {"server:owner", "/spindle/{did}", "/member", "write"}, // invite }) if err != nil { return err } // grouping policies // TODO(boltless): define our own matcher to replace keyMatch4 e.AddNamedDomainMatchingFunc("g", "keyMatch4", util.KeyMatch4) _, err = e.AddGroupingPoliciesEx([][]string{ // sub | role | dom {"repo:owner", "repo:collaborator", aturi(tangled.RepoNSID)}, // using '/knot/' prefix here because knot/spindle identifiers don't // include the collection type {"server:owner", "server:member", "/knot/{did}"}, {"server:owner", "server:member", "/spindle/{did}"}, }) return err } func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) { roles, err := e.e.GetImplicitRolesForUser(name, domain...) if err != nil { return false, err } for _, r := range roles { if r == role { return true, nil } } return false, nil } // setRoleForUser sets single user role for specified domain. // All existing users with that role will be removed. func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error { currentUsers, err := e.e.GetUsersForRole(role, domain...) if err != nil { return err } for _, oldUser := range currentUsers { _, err = e.e.DeleteRoleForUser(oldUser, role, domain...) if err != nil { return err } } _, err = e.e.AddRoleForUser(name, role, domain...) return err } // validateAtUri enforeces AT-URI to have valid did as authority and match collection NSID. func validateAtUri(uri syntax.ATURI, expected string) error { if !uri.Authority().IsDID() { return fmt.Errorf("expected at-uri with did") } if expected != "" && uri.Collection().String() != expected { return fmt.Errorf("incorrect repo at-uri collection nsid '%s' (expected '%s')", uri.Collection(), expected) } return nil }