Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}