-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(boards2): initial permissions support (#3151)
Related to #3139 Permissioner interface is defined based on Jae's idea to handle permissioned tasks. --------- Co-authored-by: Jae Kwon <[email protected]>
- Loading branch information
1 parent
c31350c
commit 4944a7e
Showing
16 changed files
with
729 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package admindao | ||
|
||
import ( | ||
"std" | ||
|
||
"gno.land/p/demo/avl" | ||
) | ||
|
||
// TODO: Add support for proposals | ||
// TODO: Add support for events | ||
|
||
// AdminDAO defines a Boards administration DAO. | ||
type AdminDAO struct { | ||
parent *AdminDAO | ||
members *avl.Tree // string(std.Address) -> struct{} | ||
} | ||
|
||
// New creates a new admin DAO. | ||
func New(options ...Option) *AdminDAO { | ||
dao := &AdminDAO{members: avl.NewTree()} | ||
for _, apply := range options { | ||
apply(dao) | ||
} | ||
return dao | ||
} | ||
|
||
// Parent returns the parent DAO. | ||
// Null can be returned when DAO has no parent assigned. | ||
func (dao AdminDAO) Parent() *AdminDAO { | ||
return dao.parent | ||
} | ||
|
||
// Members returns the list of DAO members. | ||
func (dao AdminDAO) Members() []std.Address { | ||
var members []std.Address | ||
dao.members.Iterate("", "", func(key string, _ interface{}) bool { | ||
members = append(members, std.Address(key)) | ||
return false | ||
}) | ||
return members | ||
} | ||
|
||
// IsMember checks if a user is a member of the DAO. | ||
func (dao AdminDAO) IsMember(user std.Address) bool { | ||
return dao.members.Has(user.String()) | ||
} |
88 changes: 88 additions & 0 deletions
88
examples/gno.land/p/demo/boards2/admindao/admindao_test.gno
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package admindao | ||
|
||
import ( | ||
"std" | ||
"testing" | ||
|
||
"gno.land/p/demo/uassert" | ||
"gno.land/p/demo/urequire" | ||
) | ||
|
||
func TestNew(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
parent *AdminDAO | ||
members []std.Address | ||
}{ | ||
{ | ||
name: "with parent", | ||
parent: New(), | ||
members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, | ||
}, | ||
{ | ||
name: "without parent", | ||
members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, | ||
}, | ||
{ | ||
name: "multiple members", | ||
members: []std.Address{ | ||
"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", | ||
"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", | ||
"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", | ||
}, | ||
}, | ||
{ | ||
name: "no members", | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
options := []Option{WithParent(tc.parent)} | ||
for _, m := range tc.members { | ||
options = append(options, WithMember(m)) | ||
} | ||
|
||
dao := New(options...) | ||
|
||
if tc.parent == nil { | ||
uassert.Equal(t, nil, dao.Parent()) | ||
} else { | ||
uassert.NotEqual(t, nil, dao.Parent()) | ||
} | ||
|
||
urequire.Equal(t, len(tc.members), len(dao.Members()), "dao members") | ||
for i, m := range dao.Members() { | ||
uassert.Equal(t, tc.members[i], m) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestAdminDAOIsMember(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
member std.Address | ||
dao *AdminDAO | ||
want bool | ||
}{ | ||
{ | ||
name: "member", | ||
member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", | ||
dao: New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")), | ||
want: true, | ||
}, | ||
{ | ||
name: "not a dao member", | ||
member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", | ||
dao: New(WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc")), | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
got := tc.dao.IsMember(tc.member) | ||
uassert.Equal(t, got, tc.want) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module gno.land/p/demo/boards2/admindao | ||
|
||
require ( | ||
gno.land/p/demo/avl v0.0.0-latest | ||
gno.land/p/demo/uassert v0.0.0-latest | ||
gno.land/p/demo/urequire v0.0.0-latest | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package admindao | ||
|
||
import "std" | ||
|
||
// Option configures the AdminDAO. | ||
type Option func(*AdminDAO) | ||
|
||
// WithParent assigns a parent DAO. | ||
func WithParent(p *AdminDAO) Option { | ||
return func(dao *AdminDAO) { | ||
dao.parent = p | ||
} | ||
} | ||
|
||
// WithMember assigns a member to the DAO. | ||
func WithMember(addr std.Address) Option { | ||
return func(dao *AdminDAO) { | ||
dao.members.Set(addr.String(), struct{}{}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package boards2 | ||
|
||
import ( | ||
"std" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/boards2/admindao" | ||
) | ||
|
||
// TODO: Support to deal with permissions for anonymous users? | ||
|
||
const ( | ||
RoleOwner Role = "owner" | ||
RoleAdmin = "admin" | ||
RoleModerator = "moderator" | ||
) | ||
|
||
type ( | ||
// Role defines the type for user roles. | ||
Role string | ||
|
||
// ACL or access control list manages user roles and permissions. | ||
ACL struct { | ||
superRole Role | ||
dao *admindao.AdminDAO | ||
users *avl.Tree // string(std.Address) -> []Role | ||
roles *avl.Tree // string(role) -> []Permission | ||
} | ||
) | ||
|
||
// NewACL create a new access control list. | ||
func NewACL(dao *admindao.AdminDAO, options ...ACLOption) *ACL { | ||
acl := &ACL{ | ||
dao: dao, | ||
roles: avl.NewTree(), | ||
users: avl.NewTree(), | ||
} | ||
for _, apply := range options { | ||
apply(acl) | ||
} | ||
return acl | ||
} | ||
|
||
// Roles returns the list of roles. | ||
func (acl ACL) Roles() []Role { | ||
var roles []Role | ||
acl.roles.Iterate("", "", func(name string, _ interface{}) bool { | ||
roles = append(roles, Role(name)) | ||
return false | ||
}) | ||
return roles | ||
} | ||
|
||
// GetUserRoles returns the list of roles assigned to a user. | ||
func (acl ACL) GetUserRoles(user std.Address) []Role { | ||
v, found := acl.users.Get(user.String()) | ||
if !found { | ||
return nil | ||
} | ||
return v.([]Role) | ||
} | ||
|
||
// HasRole checks if a user has a specific role assigned. | ||
func (acl ACL) HasRole(user std.Address, r Role) bool { | ||
for _, role := range acl.GetUserRoles(user) { | ||
if role == r { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// HasPermission checks if a user has a specific permission. | ||
func (acl ACL) HasPermission(user std.Address, perm Permission) bool { | ||
// TODO: Should we check that the user belongs to the DAO? | ||
for _, r := range acl.GetUserRoles(user) { | ||
v, found := acl.roles.Get(string(r)) | ||
if !found { | ||
continue | ||
} | ||
|
||
for _, p := range v.([]Permission) { | ||
if p == perm { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// WithPermission calls a callback when a user has a specific permission. | ||
// It panics on error. | ||
func (acl ACL) WithPermission(user std.Address, perm Permission, a Args, cb func(Args)) { | ||
if !acl.HasPermission(user, perm) || !acl.dao.IsMember(user) { | ||
panic("unauthorized") | ||
} | ||
|
||
// TODO: Support DAO proposals that run the callback on proposal execution | ||
cb(a) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package boards2 | ||
|
||
import "std" | ||
|
||
// ACLOption configures an ACL. | ||
type ACLOption func(*ACL) | ||
|
||
// WithSuperRole assigns a super role. | ||
// A super role is one that have all ACL permissions. | ||
// These type of role doesn't need to be mapped to any permission. | ||
func WithSuperRole(r Role) ACLOption { | ||
return func(acl *ACL) { | ||
acl.superRole = r | ||
} | ||
} | ||
|
||
// WithUser adds a user to the ACL with optional assigned roles. | ||
func WithUser(user std.Address, roles ...Role) ACLOption { | ||
return func(acl *ACL) { | ||
// TODO: Should we enforce that users are members of the DAO? [acl.dao.IsMember(user)] | ||
acl.users.Set(user.String(), append([]Role(nil), roles...)) | ||
} | ||
} | ||
|
||
// WithRole add a role to the ACL with one or more assigned permissions. | ||
func WithRole(r Role, p Permission, extra ...Permission) ACLOption { | ||
return func(acl *ACL) { | ||
acl.roles.Set(string(r), append([]Permission{p}, extra...)) | ||
} | ||
} |
Oops, something went wrong.