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 (e *Enforcer) AddRepo(member, domain, repo string) error {
100 // sanity check, repo must be of the form ownerDid/repo
101 if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
102 return fmt.Errorf("invalid repo: %s", repo)
103 }
104
105 _, err := e.E.AddPolicies([][]string{
106 {member, domain, repo, "repo:settings"},
107 {member, domain, repo, "repo:push"},
108 {member, domain, repo, "repo:owner"},
109 {member, domain, repo, "repo:invite"},
110 {member, domain, repo, "repo:delete"},
111 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
112 })
113 return err
114}
115
116func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
117 // sanity check, repo must be of the form ownerDid/repo
118 if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") {
119 return fmt.Errorf("invalid repo: %s", repo)
120 }
121
122 _, err := e.E.AddPolicies([][]string{
123 {collaborator, domain, repo, "repo:collaborator"},
124 {collaborator, domain, repo, "repo:settings"},
125 {collaborator, domain, repo, "repo:push"},
126 })
127 return err
128}
129
130func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) {
131 var membersWithoutRoles []string
132
133 // this includes roles too, casbin does not differentiate.
134 // the filtering criteria is to remove strings not starting with `did:`
135 members, err := e.E.GetImplicitUsersForRole(role, domain)
136 for _, m := range members {
137 if strings.HasPrefix(m, "did:") {
138 membersWithoutRoles = append(membersWithoutRoles, m)
139 }
140 }
141 if err != nil {
142 return nil, err
143 }
144
145 return membersWithoutRoles, nil
146}
147
148func (e *Enforcer) isRole(user, role, domain string) (bool, error) {
149 return e.E.HasGroupingPolicy(user, role, domain)
150}
151
152func (e *Enforcer) IsServerOwner(user, domain string) (bool, error) {
153 return e.isRole(user, "server:owner", domain)
154}
155
156func (e *Enforcer) IsServerMember(user, domain string) (bool, error) {
157 return e.isRole(user, "server:member", domain)
158}
159
160func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
161 return e.E.Enforce(user, domain, repo, "repo:push")
162}
163
164func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
165 return e.E.Enforce(user, domain, repo, "repo:settings")
166}
167
168// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
169func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
170 var permissions []string
171 res := e.E.GetPermissionsForUserInDomain(user, domain)
172 for _, p := range res {
173 // get only permissions for this resource/repo
174 if p[2] == repo {
175 permissions = append(permissions, p[3])
176 }
177 }
178
179 return permissions
180}
181
182func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
183 return e.E.Enforce(user, domain, repo, "repo:invite")
184}
185
186// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
187func keyMatch2Func(args ...interface{}) (interface{}, error) {
188 name1 := args[0].(string)
189 name2 := args[1].(string)
190
191 return keyMatch2(name1, name2), nil
192}