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}