Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

permissions.gno

6.93 Kb · 228 lines
  1package permissions
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/gnoland/boards/v0"
  7	"gno.land/p/nt/bptree/v0"
  8	"gno.land/p/nt/groups/v0"
  9)
 10
 11// ValidatorFunc defines a function type for permissions validators.
 12//
 13// SECURITY: validators run inside WithPermission holding the live
 14// Permissions value, with full mutation access (SetUserRoles, RemoveUser,
 15// AddRole, ...) under the owning realm's authority. Register only functions
 16// the owning realm controls, and never expose ValidateFunc or the
 17// *Permissions value across a realm boundary.
 18type ValidatorFunc func(boards.Permissions, boards.Args) error
 19
 20// Permissions manages users, roles and permissions.
 21//
 22// This type is a default `gno.land/p/gnoland/boards/v0` package `Permissions`
 23// implementation that handles boards users, roles and permissions using an
 24// underlying groups.Group: the base set holds every user (guests included),
 25// and each boards role is a group Role whose member set is kept a subset of
 26// the base set, with the role's boards.PermissionSet stored in the role meta
 27// (a value type, per the groups meta rule). It also supports optionally
 28// setting validation functions to be triggered within `WithPermission()`
 29// method before a permissioned callback is called.
 30//
 31// No permissions validation is done by default.
 32//
 33// Users are allowed to have multiple roles at the same time by default, but
 34// permissions can be configured to only allow one role per user.
 35type Permissions struct {
 36	superRole      boards.Role
 37	group          *groups.Group
 38	public         boards.PermissionSet
 39	validators     *bptree.BPTree // string(boards.Permission) -> ValidatorFunc
 40	singleUserRole bool
 41}
 42
 43// New creates a new permissions type.
 44func New(options ...Option) *Permissions {
 45	ps := &Permissions{
 46		validators: bptree.NewBPTree32(),
 47		group:      groups.NewGroup(),
 48	}
 49
 50	for _, apply := range options {
 51		apply(ps)
 52	}
 53	return ps
 54}
 55
 56// ValidateFunc adds a custom permission validator function.
 57// If an existing permission function exists it's overwritten by the new one.
 58func (ps *Permissions) ValidateFunc(p boards.Permission, fn ValidatorFunc) {
 59	ps.validators.Set(p.String(), fn)
 60}
 61
 62// SetPublicPermissions assigns permissions that are available to anyone.
 63// It removes previous public permissions and assigns the new ones.
 64// By default there are no public permissions.
 65func (ps *Permissions) SetPublicPermissions(permissions ...boards.Permission) {
 66	ps.public = boards.NewPermissionSet(permissions...)
 67}
 68
 69// AddRole adds a role with one or more assigned permissions.
 70// If role exists its permissions are overwritten with the new ones.
 71func (ps *Permissions) AddRole(r boards.Role, p boards.Permission, extra ...boards.Permission) {
 72	name := string(r)
 73	if strings.TrimSpace(name) == "" {
 74		panic("role name is required")
 75	}
 76
 77	// If role is the super role it already has all permissions
 78	if ps.superRole == r {
 79		return
 80	}
 81
 82	// Get the role if it exists or otherwise register a new one
 83	role, found := ps.group.GetRole(name)
 84	if !found {
 85		var err error
 86		role, err = ps.group.AddRole(name)
 87		if err != nil {
 88			panic(err)
 89		}
 90	}
 91
 92	// Save permissions within the role meta overwriting any existing permissions
 93	permissions := append([]boards.Permission{p}, extra...)
 94	role.SetMeta(boards.NewPermissionSet(permissions...))
 95}
 96
 97// RoleExists checks if a role exists.
 98func (ps Permissions) RoleExists(r boards.Role) bool {
 99	return r == ps.superRole || ps.group.HasRole(string(r))
100}
101
102// GetUserRoles returns the list of roles assigned to a user.
103func (ps Permissions) GetUserRoles(user address) []boards.Role {
104	names := ps.group.RolesContaining(user)
105	if names == nil {
106		return nil
107	}
108
109	roles := make([]boards.Role, len(names))
110	for i, name := range names {
111		roles[i] = boards.Role(name)
112	}
113	return roles
114}
115
116// HasRole checks if a user has a specific role assigned.
117func (ps Permissions) HasRole(user address, r boards.Role) bool {
118	role, found := ps.group.GetRole(string(r))
119	if !found {
120		return false
121	}
122	return role.Members().Has(user)
123}
124
125// HasPermission checks if a user has a specific permission.
126func (ps Permissions) HasPermission(user address, perm boards.Permission) bool {
127	if ps.public.Has(perm) {
128		return true
129	}
130
131	for _, name := range ps.group.RolesContaining(user) {
132		if ps.superRole == boards.Role(name) {
133			return true
134		}
135
136		role, found := ps.group.GetRole(name)
137		if !found {
138			continue
139		}
140
141		if perms, ok := role.Meta().(boards.PermissionSet); ok && perms.Has(perm) {
142			return true
143		}
144	}
145	return false
146}
147
148// SetUserRoles adds a new user when it doesn't exist and sets its roles.
149// Method can also be called to change the roles of an existing user.
150// It removes any existing user roles before assigning new ones.
151// All user's roles can be removed by calling this method without roles.
152func (ps *Permissions) SetUserRoles(user address, roles ...boards.Role) {
153	if len(roles) > 1 && ps.singleUserRole {
154		panic("user can only have one role")
155	}
156
157	// Resolve every role name upfront so an invalid name panics before any
158	// state is mutated.
159	newRoles := make([]*groups.Role, len(roles))
160	for i, r := range roles {
161		role, found := ps.group.GetRole(string(r))
162		if !found {
163			panic("invalid role: " + string(r))
164		}
165		newRoles[i] = role
166	}
167
168	// Clear current user roles
169	for _, name := range ps.group.RolesContaining(user) {
170		if role, found := ps.group.GetRole(name); found {
171			role.Members().Remove(user)
172		}
173	}
174
175	// Every user is a base set member, with or without roles; role member
176	// sets are kept subsets of the base set.
177	ps.group.Add(user)
178
179	// Add user to role member sets
180	for _, role := range newRoles {
181		role.Members().Add(user)
182	}
183}
184
185// RemoveUser removes a user from permissions.
186func (ps *Permissions) RemoveUser(user address) bool {
187	return ps.group.RemoveFromAll(user)
188}
189
190// HasUser checks if a user exists.
191func (ps Permissions) HasUser(user address) bool {
192	return ps.group.Has(user)
193}
194
195// UsersCount returns the total number of users the permissioner contains.
196func (ps Permissions) UsersCount() int {
197	return ps.group.Size()
198}
199
200// IterateUsers iterates permissions' users.
201func (ps Permissions) IterateUsers(start, count int, fn boards.UsersIterFn) (stopped bool) {
202	return ps.group.Iterate(start, count, func(addr address) bool {
203		return fn(boards.User{
204			Address: addr,
205			Roles:   ps.GetUserRoles(addr),
206		})
207	})
208}
209
210// WithPermission calls a callback when a user has a specific permission.
211// It panics on error or when a permission validator fails.
212// Callbacks are by default called when there is no validator function registered for the permission.
213// If a permission validation function exists it's called before calling the callback.
214func (ps *Permissions) WithPermission(user address, p boards.Permission, args boards.Args, cb func()) {
215	if !ps.HasPermission(user, p) {
216		panic("unauthorized, user " + user.String() + " doesn't have the required permission")
217	}
218
219	// Execute custom validation before calling the callback
220	if v := ps.validators.Get(p.String()); v != nil {
221		err := v.(ValidatorFunc)(ps, args)
222		if err != nil {
223			panic(err)
224		}
225	}
226
227	cb()
228}