1package rbac2
2
3import (
4 "database/sql"
5 "fmt"
6
7 adapter "github.com/Blank-Xu/sql-adapter"
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "github.com/casbin/casbin/v2"
10 "github.com/casbin/casbin/v2/model"
11 "github.com/casbin/casbin/v2/util"
12 "tangled.org/core/api/tangled"
13)
14
15const (
16 Model = `
17[request_definition]
18r = sub, dom, obj, act
19
20[policy_definition]
21p = sub, dom, obj, act
22
23[role_definition]
24g = _, _, _
25
26[policy_effect]
27e = some(where (p.eft == allow))
28
29[matchers]
30m = g(r.sub, p.sub, r.dom) && keyMatch4(r.dom, p.dom) && r.obj == p.obj && r.act == p.act
31`
32)
33
34type Enforcer struct {
35 e *casbin.Enforcer
36}
37
38func NewEnforcer(path string) (*Enforcer, error) {
39 m, err := model.NewModelFromString(Model)
40 if err != nil {
41 return nil, err
42 }
43
44 db, err := sql.Open("sqlite3", path+"?_foreign_keys=1")
45 if err != nil {
46 return nil, err
47 }
48
49 a, err := adapter.NewAdapter(db, "sqlite3", "acl")
50 if err != nil {
51 return nil, err
52 }
53
54 e, err := casbin.NewEnforcer(m, a)
55 if err != nil {
56 return nil, err
57 }
58
59 if err := seedTangledPolicies(e); err != nil {
60 return nil, err
61 }
62
63 return &Enforcer{e}, nil
64}
65
66func seedTangledPolicies(e *casbin.Enforcer) error {
67 // policies
68 aturi := func(nsid string) string {
69 return fmt.Sprintf("at://{did}/%s/{rkey}", nsid)
70 }
71
72 _, err := e.AddPoliciesEx([][]string{
73 // sub | dom | obj | act
74 {"repo:owner", aturi(tangled.RepoNSID), "/", "write"},
75 {"repo:owner", aturi(tangled.RepoNSID), "/collaborator", "write"}, // invite
76 {"repo:collaborator", aturi(tangled.RepoNSID), "/settings", "write"},
77 {"repo:collaborator", aturi(tangled.RepoNSID), "/git", "write"}, // git push
78
79 {"server:owner", "/knot/{did}", "/member", "write"}, // invite
80 {"server:member", "/knot/{did}", "/git", "write"},
81
82 {"server:owner", "/spindle/{did}", "/member", "write"}, // invite
83 })
84 if err != nil {
85 return err
86 }
87
88 // grouping policies
89 // TODO(boltless): define our own matcher to replace keyMatch4
90 e.AddNamedDomainMatchingFunc("g", "keyMatch4", util.KeyMatch4)
91 _, err = e.AddGroupingPoliciesEx([][]string{
92 // sub | role | dom
93 {"repo:owner", "repo:collaborator", aturi(tangled.RepoNSID)},
94
95 // using '/knot/' prefix here because knot/spindle identifiers don't
96 // include the collection type
97 {"server:owner", "server:member", "/knot/{did}"},
98 {"server:owner", "server:member", "/spindle/{did}"},
99 })
100 return err
101}
102
103func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
104 roles, err := e.e.GetImplicitRolesForUser(name, domain...)
105 if err != nil {
106 return false, err
107 }
108 for _, r := range roles {
109 if r == role {
110 return true, nil
111 }
112 }
113 return false, nil
114}
115
116// setRoleForUser sets single user role for specified domain.
117// All existing users with that role will be removed.
118func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error {
119 currentUsers, err := e.e.GetUsersForRole(role, domain...)
120 if err != nil {
121 return err
122 }
123
124 for _, oldUser := range currentUsers {
125 _, err = e.e.DeleteRoleForUser(oldUser, role, domain...)
126 if err != nil {
127 return err
128 }
129 }
130
131 _, err = e.e.AddRoleForUser(name, role, domain...)
132 return err
133}
134
135// validateAtUri enforeces AT-URI to have valid did as authority and match collection NSID.
136func validateAtUri(uri syntax.ATURI, expected string) error {
137 if !uri.Authority().IsDID() {
138 return fmt.Errorf("expected at-uri with did")
139 }
140 if expected != "" && uri.Collection().String() != expected {
141 return fmt.Errorf("incorrect repo at-uri collection nsid '%s' (expected '%s')", uri.Collection(), expected)
142 }
143 return nil
144}