Monorepo for Tangled
tangled.org
1package rbac
2
3import (
4 "database/sql"
5 "slices"
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 ThisServer = "thisserver" // resource identifier for local rbac enforcement
15)
16
17const (
18 Model = `
19[request_definition]
20r = sub, dom, obj, act
21
22[policy_definition]
23p = sub, dom, obj, act
24
25[role_definition]
26g = _, _, _
27
28[policy_effect]
29e = some(where (p.eft == allow))
30
31[matchers]
32m = r.act == p.act && r.dom == p.dom && r.obj == p.obj && g(r.sub, p.sub, r.dom)
33`
34)
35
36type Enforcer struct {
37 E *casbin.SyncedEnforcer
38}
39
40func NewEnforcer(path string) (*Enforcer, error) {
41 m, err := model.NewModelFromString(Model)
42 if err != nil {
43 return nil, err
44 }
45
46 db, err := sql.Open("sqlite3", path+"?_foreign_keys=1&_journal_mode=WAL&_busy_timeout=5000")
47 if err != nil {
48 return nil, err
49 }
50
51 a, err := adapter.NewAdapter(db, "sqlite3", "acl")
52 if err != nil {
53 return nil, err
54 }
55
56 e, err := casbin.NewSyncedEnforcer(m, a)
57 if err != nil {
58 return nil, err
59 }
60
61 e.EnableAutoSave(false)
62
63 return &Enforcer{e}, nil
64}
65
66func (e *Enforcer) AddKnot(knot string) error {
67 // Add policies with patterns
68 _, err := e.E.AddPolicies([][]string{
69 {"server:owner", knot, knot, "server:invite"},
70 {"server:member", knot, knot, "repo:create"},
71 })
72 if err != nil {
73 return err
74 }
75
76 // all owners are also members
77 _, err = e.E.AddGroupingPolicy("server:owner", "server:member", knot)
78 return err
79}
80
81func (e *Enforcer) AddSpindle(spindle string) error {
82 // the internal repr for spindles is spindle:foo.com
83 spindle = intoSpindle(spindle)
84
85 _, err := e.E.AddPolicies([][]string{
86 {"server:owner", spindle, spindle, "server:invite"},
87 })
88 if err != nil {
89 return err
90 }
91
92 // all owners are also members
93 _, err = e.E.AddGroupingPolicy("server:owner", "server:member", spindle)
94 return err
95}
96
97func (e *Enforcer) RemoveSpindle(spindle string) error {
98 spindle = intoSpindle(spindle)
99 _, err := e.E.DeleteDomains(spindle)
100 return err
101}
102
103func (e *Enforcer) RemoveKnot(knot string) error {
104 _, err := e.E.DeleteDomains(knot)
105 return err
106}
107
108func (e *Enforcer) GetKnotsForUser(did string) ([]string, error) {
109 keepFunc := isNotSpindle
110 stripFunc := unSpindle
111 return e.getDomainsForUser(did, keepFunc, stripFunc)
112}
113
114func (e *Enforcer) GetSpindlesForUser(did string) ([]string, error) {
115 keepFunc := isSpindle
116 stripFunc := unSpindle
117 return e.getDomainsForUser(did, keepFunc, stripFunc)
118}
119
120func (e *Enforcer) AddKnotOwner(domain, owner string) error {
121 return e.addOwner(domain, owner)
122}
123
124func (e *Enforcer) RemoveKnotOwner(domain, owner string) error {
125 return e.removeOwner(domain, owner)
126}
127
128func (e *Enforcer) AddKnotMember(domain, member string) error {
129 _, err := e.addMember(domain, member)
130 return err
131}
132
133func (e *Enforcer) RemoveKnotMember(domain, member string) error {
134 _, err := e.removeMember(domain, member)
135 return err
136}
137
138func (e *Enforcer) TryAddKnotMember(domain, member string) (bool, error) {
139 return e.addMember(domain, member)
140}
141
142func (e *Enforcer) TryRemoveKnotMember(domain, member string) (bool, error) {
143 return e.removeMember(domain, member)
144}
145
146func (e *Enforcer) AddSpindleOwner(domain, owner string) error {
147 return e.addOwner(intoSpindle(domain), owner)
148}
149
150func (e *Enforcer) RemoveSpindleOwner(domain, owner string) error {
151 return e.removeOwner(intoSpindle(domain), owner)
152}
153
154func (e *Enforcer) AddSpindleMember(domain, member string) error {
155 _, err := e.addMember(intoSpindle(domain), member)
156 return err
157}
158
159func (e *Enforcer) RemoveSpindleMember(domain, member string) error {
160 _, err := e.removeMember(intoSpindle(domain), member)
161 return err
162}
163
164func (e *Enforcer) TryAddSpindleMember(domain, member string) (bool, error) {
165 return e.addMember(intoSpindle(domain), member)
166}
167
168func (e *Enforcer) TryRemoveSpindleMember(domain, member string) (bool, error) {
169 return e.removeMember(intoSpindle(domain), member)
170}
171
172func repoPolicies(member, domain, repo string) [][]string {
173 return [][]string{
174 {member, domain, repo, "repo:settings"},
175 {member, domain, repo, "repo:push"},
176 {member, domain, repo, "repo:owner"},
177 {member, domain, repo, "repo:invite"},
178 {member, domain, repo, "repo:delete"},
179 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
180 }
181}
182func (e *Enforcer) AddRepo(member, domain, repo string) error {
183 err := checkRepoFormat(repo)
184 if err != nil {
185 return err
186 }
187
188 _, err = e.E.AddPolicies(repoPolicies(member, domain, repo))
189 return err
190}
191func (e *Enforcer) RemoveRepo(member, domain, repo string) error {
192 err := checkRepoFormat(repo)
193 if err != nil {
194 return err
195 }
196
197 _, err = e.E.RemovePolicies(repoPolicies(member, domain, repo))
198 return err
199}
200
201var (
202 collaboratorPolicies = func(collaborator, domain, repo string) [][]string {
203 return [][]string{
204 {collaborator, domain, repo, "repo:collaborator"},
205 {collaborator, domain, repo, "repo:settings"},
206 {collaborator, domain, repo, "repo:push"},
207 }
208 }
209)
210
211func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error {
212 err := checkRepoFormat(repo)
213 if err != nil {
214 return err
215 }
216
217 _, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo))
218 return err
219}
220
221func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error {
222 err := checkRepoFormat(repo)
223 if err != nil {
224 return err
225 }
226
227 _, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo))
228 return err
229}
230
231func (e *Enforcer) WipeRepoPolicies(domain, repo string) error {
232 if err := checkRepoFormat(repo); err != nil {
233 return err
234 }
235 _, err := e.E.RemoveFilteredPolicy(1, domain, repo)
236 return err
237}
238
239func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) {
240 var membersWithoutRoles []string
241
242 // this includes roles too, casbin does not differentiate.
243 // the filtering criteria is to remove strings not starting with `did:`
244 members, err := e.E.GetImplicitUsersForRole(role, domain)
245 for _, m := range members {
246 if strings.HasPrefix(m, "did:") {
247 membersWithoutRoles = append(membersWithoutRoles, m)
248 }
249 }
250 if err != nil {
251 return nil, err
252 }
253
254 slices.Sort(membersWithoutRoles)
255 return slices.Compact(membersWithoutRoles), nil
256}
257
258func (e *Enforcer) GetKnotUsersByRole(role, domain string) ([]string, error) {
259 return e.GetUserByRole(role, domain)
260}
261
262func (e *Enforcer) GetSpindleUsersByRole(role, domain string) ([]string, error) {
263 return e.GetUserByRole(role, intoSpindle(domain))
264}
265
266func (e *Enforcer) GetCollaboratorsByRepo(domain string) (map[string][]string, error) {
267 policies, err := e.E.GetFilteredNamedPolicy("p", 3, "repo:collaborator")
268 if err != nil {
269 return nil, err
270 }
271
272 byRepo := make(map[string][]string)
273 for _, p := range policies {
274 subject, dom, repo := p[0], p[1], p[2]
275 if dom != domain || !strings.HasPrefix(subject, "did:") {
276 continue
277 }
278 byRepo[repo] = append(byRepo[repo], subject)
279 }
280
281 for repo, users := range byRepo {
282 slices.Sort(users)
283 byRepo[repo] = slices.Compact(users)
284 }
285
286 return byRepo, nil
287}
288
289func (e *Enforcer) IsKnotOwner(user, domain string) (bool, error) {
290 return e.isRole(user, "server:owner", domain)
291}
292
293func (e *Enforcer) IsKnotMember(user, domain string) (bool, error) {
294 return e.isRole(user, "server:member", domain)
295}
296
297func (e *Enforcer) IsSpindleOwner(user, domain string) (bool, error) {
298 return e.isRole(user, "server:owner", intoSpindle(domain))
299}
300
301func (e *Enforcer) IsSpindleMember(user, domain string) (bool, error) {
302 return e.isRole(user, "server:member", intoSpindle(domain))
303}
304
305func (e *Enforcer) IsKnotInviteAllowed(user, domain string) (bool, error) {
306 return e.isInviteAllowed(user, domain)
307}
308
309func (e *Enforcer) IsSpindleInviteAllowed(user, domain string) (bool, error) {
310 return e.isInviteAllowed(user, intoSpindle(domain))
311}
312
313func (e *Enforcer) IsRepoCreateAllowed(user, domain string) (bool, error) {
314 return e.E.Enforce(user, domain, domain, "repo:create")
315}
316
317func (e *Enforcer) IsRepoDeleteAllowed(user, domain, repo string) (bool, error) {
318 return e.E.Enforce(user, domain, repo, "repo:delete")
319}
320
321func (e *Enforcer) IsRepoOwner(user, domain, repo string) (bool, error) {
322 return e.E.Enforce(user, domain, repo, "repo:owner")
323}
324
325func (e *Enforcer) IsRepoCollaborator(user, domain, repo string) (bool, error) {
326 return e.E.Enforce(user, domain, repo, "repo:collaborator")
327}
328
329func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
330 return e.E.Enforce(user, domain, repo, "repo:push")
331}
332
333func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) {
334 return e.E.Enforce(user, domain, repo, "repo:settings")
335}
336
337func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) {
338 return e.E.Enforce(user, domain, repo, "repo:invite")
339}
340
341// given a repo, what permissions does this user have? repo:owner? repo:invite? etc.
342func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string {
343 var permissions []string
344 res := e.E.GetPermissionsForUserInDomain(user, domain)
345 for _, p := range res {
346 // get only permissions for this resource/repo
347 if p[2] == repo {
348 permissions = append(permissions, p[3])
349 }
350 }
351
352 return permissions
353}