1package rbac
2
3import (
4 "database/sql"
5 "fmt"
6 "path"
7 "strings"
8
9 sqladapter "github.com/Blank-Xu/sql-adapter"
10 "github.com/casbin/casbin/v2"
11 "github.com/casbin/casbin/v2/model"
12)
13
14const (
15 Model = `
16[request_definition]
17r = sub, dom, obj, act
18
19[policy_definition]
20p = sub, dom, obj, act
21
22[role_definition]
23g = _, _, _
24
25[policy_effect]
26e = some(where (p.eft == allow))
27
28[matchers]
29m = r.act == p.act && r.dom == p.dom && keyMatch2(r.obj, p.obj) && g(r.sub, p.sub, r.dom)
30`
31)
32
33type Enforcer struct {
34 E *casbin.SyncedEnforcer
35}
36
37func keyMatch2(key1 string, key2 string) bool {
38 matched, _ := path.Match(key2, key1)
39 return matched
40}
41
42func NewEnforcer(path string) (*Enforcer, error) {
43 m, err := model.NewModelFromString(Model)
44 if err != nil {
45 return nil, err
46 }
47
48 db, err := sql.Open("sqlite3", path)
49 if err != nil {
50 return nil, err
51 }
52
53 a, err := sqladapter.NewAdapter(db, "sqlite3", "acl")
54 if err != nil {
55 return nil, err
56 }
57
58 e, err := casbin.NewSyncedEnforcer(m, a)
59 if err != nil {
60 return nil, err
61 }
62
63 e.EnableAutoSave(true)
64 e.AddFunction("keyMatch2", keyMatch2Func)
65
66 return &Enforcer{e}, nil
67}
68
69func (e *Enforcer) AddDomain(domain string) error {
70 // Add policies with patterns
71 _, err := e.E.AddPolicies([][]string{
72 {"server:owner", domain, domain, "server:invite"},
73 {"server:member", domain, domain, "repo:create"},
74 })
75 if err != nil {
76 return err
77 }
78
79 // all owners are also members
80 _, err = e.E.AddGroupingPolicy("server:owner", "server:member", domain)
81 return err
82}
83
84func (e *Enforcer) GetDomainsForUser(did string) ([]string, error) {
85 return e.E.Enforcer.GetDomainsForUser(did)
86}
87
88func (e *Enforcer) AddOwner(domain, owner string) error {
89 _, err := e.E.AddGroupingPolicy(owner, "server:owner", domain)
90 return err
91}
92
93func (e *Enforcer) AddMember(domain, member string) error {
94 _, err := e.E.AddGroupingPolicy(member, "server:member", domain)
95 return err
96}
97
98func (e *Enforcer) AddRepo(member, domain, repo string) error {
99 // sanity check, repo must be of the form ownerDid/repo
100 if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
101 return fmt.Errorf("invalid repo: %s", repo)
102 }
103
104 _, err := e.E.AddPolicies([][]string{
105 {member, domain, repo, "repo:settings"},
106 {member, domain, repo, "repo:push"},
107 {member, domain, repo, "repo:owner"},
108 {member, domain, repo, "repo:invite"},
109 {member, domain, repo, "repo:delete"},
110 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
111 })
112 return err
113}
114
115func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
116 // sanity check, repo must be of the form ownerDid/repo
117 if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
118 return fmt.Errorf("invalid repo: %s", repo)
119 }
120
121 _, err := e.E.AddPolicies([][]string{
122 {collaborator, domain, repo, "repo:collaborator"},
123 {collaborator, domain, repo, "repo:settings"},
124 {collaborator, domain, repo, "repo:push"},
125 })
126 return err
127}
128
129func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) {
130 var membersWithoutRoles []string
131
132 // this includes roles too, casbin does not differentiate.
133 // the filtering criteria is to remove strings not starting with `did:`
134 members, err := e.E.Enforcer.GetImplicitUsersForRole(role, domain)
135 for _, m := range members {
136 if strings.HasPrefix(m, "did:") {
137 membersWithoutRoles = append(membersWithoutRoles, m)
138 }
139 }
140 if err != nil {
141 return nil, err
142 }
143
144 return membersWithoutRoles, nil
145}
146
147func (e *Enforcer) isRole(user, role, domain string) (bool, error) {
148 return e.E.HasGroupingPolicy(user, role, domain)
149}
150
151func (e *Enforcer) IsServerOwner(user, domain string) (bool, error) {
152 return e.isRole(user, "server:owner", domain)
153}
154
155func (e *Enforcer) IsServerMember(user, domain string) (bool, error) {
156 return e.isRole(user, "server:member", domain)
157}
158
159func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
160 return e.E.Enforce(user, domain, repo, "repo:push")
161}
162
163func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
164 return e.E.Enforce(user, domain, repo, "repo:settings")
165}
166
167func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
168 return e.E.Enforce(user, domain, repo, "repo:invite")
169}
170
171// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
172func keyMatch2Func(args ...interface{}) (interface{}, error) {
173 name1 := args[0].(string)
174 name2 := args[1].(string)
175
176 return keyMatch2(name1, name2), nil
177}