Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}