permissions.gno
7.14 Kb ยท 260 lines
1package permissions
2
3import (
4 "gno.land/p/gnoland/boards"
5 "gno.land/p/nt/avl"
6 "gno.land/p/nt/commondao"
7 "gno.land/p/nt/commondao/exts/storage"
8)
9
10// ValidatorFunc defines a function type for permissions validators.
11type ValidatorFunc func(boards.Permissions, boards.Args) error
12
13// Permissions manages users, roles and permissions.
14//
15// This type is a default `gno.land/p/gnoland/boards` package `Permissions` implementation
16// that handles boards users, roles and permissions using an underlying DAO. It also supports
17// optionally setting validation functions to be triggered within `WithPermission()` method
18// before a permissioned callback is called.
19//
20// No permissions validation is done by default.
21//
22// Users are allowed to have multiple roles at the same time by default, but permissions can
23// be configured to only allow one role per user.
24type Permissions struct {
25 superRole boards.Role
26 dao *commondao.CommonDAO
27 validators *avl.Tree // string(boards.Permission) -> BasicPermissionValidator
28 public *avl.Tree // string(boards.Permission) -> struct{}{}
29 singleUserRole bool
30}
31
32// New creates a new permissions type.
33func New(options ...Option) *Permissions {
34 s := storage.NewMemberStorage()
35 ps := &Permissions{
36 validators: avl.NewTree(),
37 public: avl.NewTree(),
38 dao: commondao.New(commondao.WithMemberStorage(s)),
39 }
40
41 for _, apply := range options {
42 apply(ps)
43 }
44 return ps
45}
46
47// DAO returns the underlying permissions DAO.
48func (ps Permissions) DAO() *commondao.CommonDAO {
49 return ps.dao
50}
51
52// ValidateFunc adds a custom permission validator function.
53// If an existing permission function exists it's ovewritten by the new one.
54func (ps *Permissions) ValidateFunc(p boards.Permission, fn ValidatorFunc) {
55 ps.validators.Set(string(p), fn)
56}
57
58// SetPublicPermissions assigns permissions that are available to anyone.
59// It removes previous public permissions and assigns the new ones.
60// By default there are no public permissions.
61func (ps *Permissions) SetPublicPermissions(permissions ...boards.Permission) {
62 ps.public = avl.NewTree()
63 for _, p := range permissions {
64 ps.public.Set(string(p), struct{}{})
65 }
66}
67
68// AddRole add a role with one or more assigned permissions.
69// If role exists its permissions are overwritten with the new ones.
70func (ps *Permissions) AddRole(r boards.Role, p boards.Permission, extra ...boards.Permission) {
71 // If role is the super role it already has all permissions
72 if ps.superRole == r {
73 return
74 }
75
76 // Get member group for the role if it exists or otherwise create a new group
77 grouping := ps.dao.Members().Grouping()
78 name := string(r)
79 group, found := grouping.Get(name)
80 if !found {
81 var err error
82 group, err = grouping.Add(name)
83 if err != nil {
84 panic(err)
85 }
86 }
87
88 // Save permissions within the member group overwritting any existing permissions
89 group.SetMeta(append([]boards.Permission{p}, extra...))
90}
91
92// RoleExists checks if a role exists.
93func (ps Permissions) RoleExists(r boards.Role) bool {
94 return r == ps.superRole || ps.dao.Members().Grouping().Has(string(r))
95}
96
97// GetUserRoles returns the list of roles assigned to a user.
98func (ps Permissions) GetUserRoles(user address) []boards.Role {
99 groups := storage.GetMemberGroups(ps.dao.Members(), user)
100 if groups == nil {
101 return nil
102 }
103
104 roles := make([]boards.Role, len(groups))
105 for i, name := range groups {
106 roles[i] = boards.Role(name)
107 }
108 return roles
109}
110
111// HasRole checks if a user has a specific role assigned.
112func (ps Permissions) HasRole(user address, r boards.Role) bool {
113 name := string(r)
114 group, found := ps.dao.Members().Grouping().Get(name)
115 if !found {
116 return false
117 }
118 return group.Members().Has(user)
119}
120
121// HasPermission checks if a user has a specific permission.
122func (ps Permissions) HasPermission(user address, perm boards.Permission) bool {
123 if ps.public.Has(string(perm)) {
124 return true
125 }
126
127 groups := storage.GetMemberGroups(ps.dao.Members(), user)
128 if groups == nil {
129 return false
130 }
131
132 grouping := ps.dao.Members().Grouping()
133 for _, name := range groups {
134 role := boards.Role(name)
135 if ps.superRole == role {
136 return true
137 }
138
139 group, found := grouping.Get(name)
140 if !found {
141 continue
142 }
143
144 meta := group.GetMeta()
145 for _, p := range meta.([]boards.Permission) {
146 if p == perm {
147 return true
148 }
149 }
150 }
151 return false
152}
153
154// SetUserRoles adds a new user when it doesn't exist and sets its roles.
155// Method can also be called to change the roles of an existing user.
156// It removes any existing user roles before assigning new ones.
157// All user's roles can be removed by calling this method without roles.
158func (ps *Permissions) SetUserRoles(_ realm, user address, roles ...boards.Role) {
159 if len(roles) > 1 && ps.singleUserRole {
160 panic("user can only have one role")
161 }
162
163 groups := storage.GetMemberGroups(ps.dao.Members(), user)
164 isGuest := len(roles) == 0
165
166 // Clear current user roles
167 grouping := ps.dao.Members().Grouping()
168 for _, name := range groups {
169 group, found := grouping.Get(name)
170 if !found {
171 continue
172 }
173
174 group.Members().Remove(user)
175 }
176
177 // Add user to the storage as guest when no roles are assigned
178 if isGuest {
179 ps.dao.Members().Add(user)
180 return
181 }
182
183 // Add user to role groups
184 for _, r := range roles {
185 name := string(r)
186 group, found := grouping.Get(name)
187 if !found {
188 panic("invalid role: " + name)
189 }
190
191 group.Members().Add(user)
192 }
193}
194
195// RemoveUser removes a user from permissions.
196func (ps *Permissions) RemoveUser(_ realm, user address) bool {
197 groups := storage.GetMemberGroups(ps.dao.Members(), user)
198 if groups == nil {
199 return ps.dao.Members().Remove(user)
200 }
201
202 grouping := ps.dao.Members().Grouping()
203 for _, name := range groups {
204 group, found := grouping.Get(name)
205 if !found {
206 continue
207 }
208
209 group.Members().Remove(user)
210 }
211 return true
212}
213
214// HasUser checks if a user exists.
215func (ps Permissions) HasUser(user address) bool {
216 return ps.dao.Members().Has(user)
217}
218
219// UsersCount returns the total number of users the permissioner contains.
220func (ps Permissions) UsersCount() int {
221 return ps.dao.Members().Size()
222}
223
224// IterateUsers iterates permissions' users.
225func (ps Permissions) IterateUsers(start, count int, fn boards.UsersIterFn) (stopped bool) {
226 ps.dao.Members().IterateByOffset(start, count, func(addr address) bool {
227 user := boards.User{Address: addr}
228 groups := storage.GetMemberGroups(ps.dao.Members(), addr)
229 if groups != nil {
230 user.Roles = make([]boards.Role, len(groups))
231 for i, name := range groups {
232 user.Roles[i] = boards.Role(name)
233 }
234 }
235
236 return fn(user)
237 })
238 return
239}
240
241// WithPermission calls a callback when a user has a specific permission.
242// It panics on error or when a permission validator fails.
243// Callbacks are by default called when there is no validator registered for the permission.
244// If a permission validation function exists it's called before calling the callback.
245func (ps *Permissions) WithPermission(_ realm, user address, p boards.Permission, args boards.Args, cb func(realm)) {
246 if !ps.HasPermission(user, p) {
247 panic("unauthorized")
248 }
249
250 // Execute custom validation before calling the callback
251 v, found := ps.validators.Get(string(p))
252 if found {
253 err := v.(ValidatorFunc)(ps, args)
254 if err != nil {
255 panic(err)
256 }
257 }
258
259 cb(cross)
260}