Monorepo for Tangled
tangled.org
1package rbac2
2
3import (
4 "database/sql"
5 _ "embed"
6 "fmt"
7
8 adapter "github.com/Blank-Xu/sql-adapter"
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "github.com/casbin/casbin/v2"
11 "github.com/casbin/casbin/v2/model"
12 "github.com/casbin/casbin/v2/util"
13 "tangled.org/core/rbac2/bytesadapter"
14)
15
16const (
17 Model = `
18[request_definition]
19r = sub, dom, obj, act
20
21[policy_definition]
22p = sub, dom, obj, act
23
24[role_definition]
25g = _, _, _
26
27[policy_effect]
28e = some(where (p.eft == allow))
29
30[matchers]
31m = g(r.sub, p.sub, r.dom) && keyMatch4(r.dom, p.dom) && r.obj == p.obj && r.act == p.act
32`
33)
34
35type Enforcer struct {
36 e *casbin.Enforcer
37}
38
39//go:embed tangled_policy.csv
40var tangledPolicy []byte
41
42func NewEnforcer(path string) (*Enforcer, error) {
43 db, err := sql.Open("sqlite3", path+"?_foreign_keys=1")
44 if err != nil {
45 return nil, err
46 }
47 return NewEnforcerWithDB(db)
48}
49
50func NewEnforcerWithDB(db *sql.DB) (*Enforcer, error) {
51 m, err := model.NewModelFromString(Model)
52 if err != nil {
53 return nil, err
54 }
55
56 a, err := adapter.NewAdapter(db, "sqlite3", "acl")
57 if err != nil {
58 return nil, err
59 }
60
61 // // PATCH: create unique index to make `AddPoliciesEx` work
62 // _, err = db.Exec(fmt.Sprintf(
63 // `create unique index if not exists uq_%[1]s on %[1]s (p_type,v0,v1,v2,v3,v4,v5);`,
64 // tableName,
65 // ))
66 // if err != nil {
67 // return nil, err
68 // }
69
70 e, _ := casbin.NewEnforcer() // NewEnforcer() without param won't return error
71 // e.EnableLog(true)
72
73 // NOTE: casbin clears the model on init, so we should intialize with temporary adapter first
74 // and then override the adapter to sql-adapter.
75 // `e.SetModel(m)` after init doesn't work for some reason
76 if err := e.InitWithModelAndAdapter(m, bytesadapter.NewAdapter(tangledPolicy)); err != nil {
77 return nil, err
78 }
79
80 // load dynamic policy from db
81 e.EnableAutoSave(false)
82 if err := a.LoadPolicy(e.GetModel()); err != nil {
83 return nil, err
84 }
85 e.AddNamedDomainMatchingFunc("g", "keyMatch4", util.KeyMatch4)
86 e.BuildRoleLinks()
87 e.SetAdapter(a)
88 e.EnableAutoSave(true)
89
90 return &Enforcer{e}, nil
91}
92
93// CaptureModel returns copy of current model. Used for testing
94func (e *Enforcer) CaptureModel() model.Model {
95 return e.e.GetModel().Copy()
96}
97
98func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
99 roles, err := e.e.GetImplicitRolesForUser(name, domain...)
100 if err != nil {
101 return false, err
102 }
103 for _, r := range roles {
104 if r == role {
105 return true, nil
106 }
107 }
108 return false, nil
109}
110
111// setRoleForUser sets single user role for specified domain.
112// All existing users with that role will be removed.
113func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error {
114 currentUsers, err := e.e.GetUsersForRole(role, domain...)
115 if err != nil {
116 return err
117 }
118
119 for _, oldUser := range currentUsers {
120 _, err = e.e.DeleteRoleForUser(oldUser, role, domain...)
121 if err != nil {
122 return err
123 }
124 }
125
126 _, err = e.e.AddRoleForUser(name, role, domain...)
127 return err
128}
129
130// validateAtUri enforeces AT-URI to have valid did as authority and match collection NSID.
131func validateAtUri(uri syntax.ATURI, expected string) error {
132 if !uri.Authority().IsDID() {
133 return fmt.Errorf("expected at-uri with did")
134 }
135 if expected != "" && uri.Collection().String() != expected {
136 return fmt.Errorf("incorrect repo at-uri collection nsid '%s' (expected '%s')", uri.Collection(), expected)
137 }
138 return nil
139}