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