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