1package rbac
2
3import (
4 "database/sql"
5 "fmt"
6 "path"
7 "strings"
8
9 adapter "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.Enforcer
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 := adapter.NewAdapter(db, "sqlite3", "acl")
54 if err != nil {
55 return nil, err
56 }
57
58 e, err := casbin.NewEnforcer(m, a)
59 if err != nil {
60 return nil, err
61 }
62
63 e.EnableAutoSave(false)
64
65 e.AddFunction("keyMatch2", keyMatch2Func)
66
67 return &Enforcer{e}, nil
68}
69
70func (e *Enforcer) AddDomain(domain string) error {
71 // Add policies with patterns
72 _, err := e.E.AddPolicies([][]string{
73 {"server:owner", domain, domain, "server:invite"},
74 {"server:member", domain, domain, "repo:create"},
75 })
76 if err != nil {
77 return err
78 }
79
80 // all owners are also members
81 _, err = e.E.AddGroupingPolicy("server:owner", "server:member", domain)
82 return err
83}
84
85func (e *Enforcer) GetDomainsForUser(did string) ([]string, error) {
86 return e.E.GetDomainsForUser(did)
87}
88
89func (e *Enforcer) AddOwner(domain, owner string) error {
90 _, err := e.E.AddGroupingPolicy(owner, "server:owner", domain)
91 return err
92}
93
94func (e *Enforcer) AddMember(domain, member string) error {
95 _, err := e.E.AddGroupingPolicy(member, "server:member", domain)
96 return err
97}
98
99func repoPolicies(member, domain, repo string) [][]string {
100 return [][]string{
101 {member, domain, repo, "repo:settings"},
102 {member, domain, repo, "repo:push"},
103 {member, domain, repo, "repo:owner"},
104 {member, domain, repo, "repo:invite"},
105 {member, domain, repo, "repo:delete"},
106 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
107 }
108}
109func (e *Enforcer) AddRepo(member, domain, repo string) error {
110 err := checkRepoFormat(repo)
111 if err != nil {
112 return err
113 }
114
115 _, err = e.E.AddPolicies(repoPolicies(member, domain, repo))
116 return err
117}
118func (e *Enforcer) RemoveRepo(member, domain, repo string) error {
119 err := checkRepoFormat(repo)
120 if err != nil {
121 return err
122 }
123
124 _, err = e.E.RemovePolicies(repoPolicies(member, domain, repo))
125 return err
126}
127
128var (
129 collaboratorPolicies = func(collaborator, domain, repo string) [][]string {
130 return [][]string{
131 {collaborator, domain, repo, "repo:collaborator"},
132 {collaborator, domain, repo, "repo:settings"},
133 {collaborator, domain, repo, "repo:push"},
134 }
135 }
136)
137
138func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
139 err := checkRepoFormat(repo)
140 if err != nil {
141 return err
142 }
143
144 _, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo))
145 return err
146}
147
148func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error {
149 err := checkRepoFormat(repo)
150 if err != nil {
151 return err
152 }
153
154 _, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo))
155 return err
156}
157
158func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) {
159 var membersWithoutRoles []string
160
161 // this includes roles too, casbin does not differentiate.
162 // the filtering criteria is to remove strings not starting with `did:`
163 members, err := e.E.GetImplicitUsersForRole(role, domain)
164 for _, m := range members {
165 if strings.HasPrefix(m, "did:") {
166 membersWithoutRoles = append(membersWithoutRoles, m)
167 }
168 }
169 if err != nil {
170 return nil, err
171 }
172
173 return membersWithoutRoles, nil
174}
175
176func (e *Enforcer) isRole(user, role, domain string) (bool, error) {
177 return e.E.HasGroupingPolicy(user, role, domain)
178}
179
180func (e *Enforcer) IsServerOwner(user, domain string) (bool, error) {
181 return e.isRole(user, "server:owner", domain)
182}
183
184func (e *Enforcer) IsServerMember(user, domain string) (bool, error) {
185 return e.isRole(user, "server:member", domain)
186}
187
188func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
189 return e.E.Enforce(user, domain, repo, "repo:push")
190}
191
192func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
193 return e.E.Enforce(user, domain, repo, "repo:settings")
194}
195
196func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
197 return e.E.Enforce(user, domain, repo, "repo:invite")
198}
199
200// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
201func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
202 var permissions []string
203 res := e.E.GetPermissionsForUserInDomain(user, domain)
204 for _, p := range res {
205 // get only permissions for this resource/repo
206 if p[2] == repo {
207 permissions = append(permissions, p[3])
208 }
209 }
210
211 return permissions
212}
213
214// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
215func keyMatch2Func(args ...interface{}) (interface{}, error) {
216 name1 := args[0].(string)
217 name2 := args[1].(string)
218
219 return keyMatch2(name1, name2), nil
220}
221
222func checkRepoFormat(repo string) error {
223 // sanity check, repo must be of the form ownerDid/repo
224 if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
225 return fmt.Errorf("invalid repo: %s", repo)
226 }
227
228 return nil
229}