From b23d3787656d7a324635431d3851c0a106fce9a3 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 14:58:56 +0100 Subject: [PATCH 1/8] chore: change permissions WithUser option to add it as DAO member This allows to create the default permissions with an empty DAO, simplifying permissions initialization and removing some redundancy. --- examples/gno.land/r/demo/boards2/permission_options.gno | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/boards2/permission_options.gno b/examples/gno.land/r/demo/boards2/permission_options.gno index fdaef050206..08cacf02582 100644 --- a/examples/gno.land/r/demo/boards2/permission_options.gno +++ b/examples/gno.land/r/demo/boards2/permission_options.gno @@ -17,7 +17,10 @@ func WithSuperRole(r Role) DefaultPermissionsOption { // WithUser adds a user to default permissions with optional assigned roles. func WithUser(user std.Address, roles ...Role) DefaultPermissionsOption { return func(dp *DefaultPermissions) { - // TODO: Should we enforce that users are members of the DAO? [dp.dao.IsMember(user)] + if !dp.dao.IsMember(user) { + dp.dao.AddMember(user) + } + dp.users.Set(user.String(), append([]Role(nil), roles...)) } } From b421d4e6184ea40cdc3bf693c6fe6fe7a60fc964 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 15:00:39 +0100 Subject: [PATCH 2/8] chore: update TODOs --- examples/gno.land/r/demo/boards2/public.gno | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index ebc39a45bf0..81e3a63bb17 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -87,7 +87,7 @@ func CreateThread(bid BoardID, title, body string) PostID { func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() - // TODO: Assert that caller is a board member (when board type is invite only) + // TODO: Assert that caller is a board member when board type is invite only caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) @@ -168,8 +168,8 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { thread := mustGetThread(board, threadID) assertReplyExists(thread, replyID) - // TODO: Hide reply when the caller is the owner of the reply (remove WithPermission call for now) - // TODO: Support removing reply and children though proposals? (WithPermission) + // TODO: Hide reply when the caller is the owner of the reply without permission + // TODO: Support removing reply and children though proposals? caller := std.GetOrigCaller() args := Args{bid, threadID, replyID} @@ -186,6 +186,8 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { board := mustGetBoard(bid) assertThreadExists(board, threadID) + // TODO: Thread owners should be able to edit without permission? + caller := std.GetOrigCaller() args := Args{bid, threadID, title, body} gPerm.WithPermission(caller, PermissionThreadEdit, args, func(Args) { From f78ce9ca964b7419298c2426268088941f7cf2f0 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 15:01:11 +0100 Subject: [PATCH 3/8] feat: update role and permissions for super and boards DAOs Both initializations are similar but the permissions are different, for example board creation doesn't exist in board DAOs. Other super DAO permissions won't exist in board DAOs. --- examples/gno.land/r/demo/boards2/board.gno | 30 ++++++++++++++--- .../r/demo/boards2/permission_default.gno | 32 +++++++++++++++---- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index d6a5306860c..5dae2aaacf5 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -131,14 +131,34 @@ func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "bid", board.id.String()) } -// TODO: This is a temporary implementation until the permissions and DAO mecahnics are defined func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember(owner)), + admindao.New(), WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionMemberInvite, PermissionBoardRename), - // TODO: Finish assigning all roles and permissions - // WithRole(RoleModerator, permissions...), + WithRole( + RoleAdmin, + PermissionBoardRename, + PermissionMemberInvite, + PermissionMemberRemove, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadDelete, + PermissionThreadFlag, + PermissionReplyDelete, + PermissionReplyFlag, + ), + WithRole( + RoleModerator, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadFlag, + PermissionReplyFlag, + ), + WithRole( + RoleGuest, + PermissionThreadCreate, + PermissionThreadRepost, + ), WithUser(owner, RoleOwner), ) } diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index a98b2fdb6f3..89ba42ed180 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -193,14 +193,34 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { } func createDefaultPermissions(owner std.Address) *DefaultPermissions { - // TODO: DAO should be a different realm or proposal and voting functions should be part of boards realm? - // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissions`?? return NewDefaultPermissions( - admindao.New(admindao.WithMember(owner)), + admindao.New(), WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), - // TODO: Finish assigning all roles and permissions - // WithRole(RoleModerator, permissions...), + WithRole( + RoleAdmin, + PermissionBoardCreate, + PermissionBoardRename, + PermissionMemberInvite, + PermissionMemberRemove, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadDelete, + PermissionThreadFlag, + PermissionReplyDelete, + PermissionReplyFlag, + ), + WithRole( + RoleModerator, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadFlag, + PermissionReplyFlag, + ), + WithRole( + RoleGuest, + PermissionThreadCreate, + PermissionThreadRepost, + ), WithUser(owner, RoleOwner), ) } From ddbd0c850adcc504869316ca9f86d95b8f7c6ae1 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 15:41:24 +0100 Subject: [PATCH 4/8] test: fix unit tests --- .../r/demo/boards2/permission_default_test.gno | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index a3c51fb6abe..d71dec37c79 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -42,7 +42,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -54,7 +54,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { permission: "bar", args: Args{"a", "b"}, perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -65,7 +65,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), WithRole("foo", "bar"), ), @@ -75,12 +75,8 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - err: "unauthorized", + perms: NewDefaultPermissions(admindao.New()), + err: "unauthorized", }, } @@ -314,7 +310,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, @@ -376,7 +372,7 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, From abce625fcf7f6cf221254731d21d8ce3291eb065 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 15:50:28 +0100 Subject: [PATCH 5/8] feat: change Permissions interface to support changing user roles --- .../gno.land/r/demo/boards2/permission.gno | 3 + .../r/demo/boards2/permission_default.gno | 13 ++- .../demo/boards2/permission_default_test.gno | 99 +++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index e9a91b7b670..7c7032983dc 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -52,6 +52,9 @@ type ( // AddUser adds a new user to the permissioner. AddUser(std.Address, ...Role) error + // SetUserRoles sets the roles of a user. + SetUserRoles(std.Address, ...Role) error + // RemoveUser removes a user from the permissioner. RemoveUser(std.Address) (removed bool) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index 89ba42ed180..e500ee3670d 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -104,16 +104,21 @@ func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { return errors.New("user already exists") } + if err := dp.dao.AddMember(user); err != nil { + return err + } + + return dp.SetUserRoles(user, roles...) +} + +// SetUserRoles sets the roles of a user. +func (dp *DefaultPermissions) SetUserRoles(user std.Address, roles ...Role) error { for _, r := range roles { if !dp.RoleExists(r) { return errors.New("invalid role: " + string(r)) } } - if err := dp.dao.AddMember(user); err != nil { - return err - } - dp.users.Set(user.String(), append([]Role(nil), roles...)) return nil } diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index d71dec37c79..410c5789799 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -360,6 +360,105 @@ func TestDefaultPermissionsAddUser(t *testing.T) { } } +func TestDefaultPermissionsSetUserRoles(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []Role + setup func() *DefaultPermissions + err string + }{ + { + name: "single role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"b"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + }, + { + name: "multiple roles", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"b", "c"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithRole("c", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + }, + { + name: "duplicated role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "c"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithRole("c", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "c"), + ) + }, + }, + { + name: "remove roles", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b"), + ) + }, + }, + { + name: "invalid role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"x", "a"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + err: "invalid role: x", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + + err := perm.SetUserRoles(tc.user, tc.roles...) + + if tc.err != "" { + urequire.True(t, err != nil, "expected an error") + uassert.Equal(t, tc.err, err.Error()) + return + } else { + urequire.NoError(t, err) + } + + roles := perm.GetUserRoles(tc.user) + uassert.Equal(t, len(tc.roles), len(roles)) + for i, r := range roles { + urequire.Equal(t, string(tc.roles[i]), string(r)) + } + }) + } +} + func TestDefaultPermissionsRemoveUser(t *testing.T) { cases := []struct { name string From c5d5187cd774f1e9a97f65286382f98a2b841395 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 15:57:44 +0100 Subject: [PATCH 6/8] test: change set user roles to check user exists --- examples/gno.land/r/demo/boards2/permission_default.gno | 4 ++++ .../gno.land/r/demo/boards2/permission_default_test.gno | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index e500ee3670d..b63ce482140 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -113,6 +113,10 @@ func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { // SetUserRoles sets the roles of a user. func (dp *DefaultPermissions) SetUserRoles(user std.Address, roles ...Role) error { + if !dp.users.Has(user.String()) { + return errors.New("user not found") + } + for _, r := range roles { if !dp.RoleExists(r) { return errors.New("invalid role: " + string(r)) diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index 410c5789799..33b63cdcadf 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -434,6 +434,14 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { }, err: "invalid role: x", }, + { + name: "user not found", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New()) + }, + err: "user not found", + }, } for _, tc := range cases { From 1c384bb8a83322db78014be7c48ceb0ff3214948 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 17:04:54 +0100 Subject: [PATCH 7/8] feat: support changing member roles --- examples/gno.land/r/demo/boards2/board.gno | 1 + .../gno.land/r/demo/boards2/permission.gno | 4 ++ .../r/demo/boards2/permission_default.gno | 59 +++++++++++++++---- examples/gno.land/r/demo/boards2/public.gno | 13 ++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 5dae2aaacf5..75469943398 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -146,6 +146,7 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { PermissionThreadFlag, PermissionReplyDelete, PermissionReplyFlag, + PermissionRoleChange, ), WithRole( RoleModerator, diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 7c7032983dc..aaae6d463b6 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -18,6 +18,7 @@ const ( PermissionReplyFlag = "reply:flag" PermissionMemberInvite = "member:invite" PermissionMemberRemove = "member:remove" + PermissionRoleChange = "role:change" ) const ( @@ -58,6 +59,9 @@ type ( // RemoveUser removes a user from the permissioner. RemoveUser(std.Address) (removed bool) + // HasUser checks if a user exists. + HasUser(std.Address) bool + // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. GetDAO() *admindao.AdminDAO // TODO: should return an interface diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index b63ce482140..b99e3c0b53a 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -107,8 +107,7 @@ func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { if err := dp.dao.AddMember(user); err != nil { return err } - - return dp.SetUserRoles(user, roles...) + return dp.setUserRoles(user, roles...) } // SetUserRoles sets the roles of a user. @@ -116,15 +115,7 @@ func (dp *DefaultPermissions) SetUserRoles(user std.Address, roles ...Role) erro if !dp.users.Has(user.String()) { return errors.New("user not found") } - - for _, r := range roles { - if !dp.RoleExists(r) { - return errors.New("invalid role: " + string(r)) - } - } - - dp.users.Set(user.String(), append([]Role(nil), roles...)) - return nil + return dp.setUserRoles(user, roles...) } // RemoveUser removes a user from permissions. @@ -134,6 +125,11 @@ func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { return removed } +// HasUser checks if a user exists. +func (dp DefaultPermissions) HasUser(user std.Address) bool { + return dp.users.Has(user.String()) +} + // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. func (dp DefaultPermissions) GetDAO() *admindao.AdminDAO { @@ -155,11 +151,24 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, dp.handleBoardRename(args, cb) case PermissionMemberInvite: dp.handleMemberInvite(args, cb) + case PermissionRoleChange: + dp.handleRoleChange(args, cb) default: cb(args) } } +func (dp *DefaultPermissions) setUserRoles(user std.Address, roles ...Role) error { + for _, r := range roles { + if !dp.RoleExists(r) { + return errors.New("invalid role: " + string(r)) + } + } + + dp.users.Set(user.String(), append([]Role(nil), roles...)) + return nil +} + func (DefaultPermissions) handleBoardCreate(args Args, cb func(Args)) { name, ok := args[0].(string) if !ok { @@ -201,6 +210,33 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { cb(args) } +func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { + // Owners and Admins can change roles. + // Admins should not be able to assign or remove the Owner role from members. + caller := std.GetOrigCaller() + if dp.HasRole(caller, RoleAdmin) { + role, ok := args[1].(Role) + if !ok { + panic("expected a valid member role") + } + + if role == RoleOwner { + panic("admins are not allowed to promote members to Owner") + } else { + member, ok := args[0].(std.Address) + if !ok { + panic("expected a valid member address") + } + + if dp.HasRole(member, RoleOwner) { + panic("admins are not allowed to remove the Owner role") + } + } + } + + cb(args) +} + func createDefaultPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( admindao.New(), @@ -217,6 +253,7 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { PermissionThreadFlag, PermissionReplyDelete, PermissionReplyFlag, + PermissionRoleChange, ), WithRole( RoleModerator, diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 81e3a63bb17..b21bf1555cb 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -235,6 +235,19 @@ func RemoveMember(user std.Address) { }) } +// TODO: Allow changing board user roles +func ChangeMemberRole(member std.Address, role Role) { + assertIsUserCall() + + caller := std.GetOrigCaller() + args := Args{member, role} + gPerm.WithPermission(caller, PermissionRoleChange, args, func(Args) { + if err := gPerm.SetUserRoles(member, role); err != nil { + panic(err) + } + }) +} + func assertIsUserCall() { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") From bedfc6f39743232f91be15ba19182aef35a475ee Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 14 Jan 2025 18:40:12 +0100 Subject: [PATCH 8/8] test: add file tests for `ChangeMemberRole()` --- .../r/demo/boards2/permission_default.gno | 18 +-------- .../demo/boards2/permission_default_test.gno | 5 --- examples/gno.land/r/demo/boards2/public.gno | 37 ++++++++++++------ .../r/demo/boards2/z_1_a_filetest.gno | 3 +- .../r/demo/boards2/z_1_b_filetest.gno | 5 ++- .../r/demo/boards2/z_1_c_filetest.gno | 5 ++- .../r/demo/boards2/z_3_f_filetest.gno | 3 +- .../r/demo/boards2/z_3_g_filetest.gno | 5 ++- .../r/demo/boards2/z_4_a_filetest.gno | 36 ++++++++++++++++++ .../r/demo/boards2/z_4_b_filetest.gno | 38 +++++++++++++++++++ .../r/demo/boards2/z_4_c_filetest.gno | 29 ++++++++++++++ .../r/demo/boards2/z_4_d_filetest.gno | 29 ++++++++++++++ .../r/demo/boards2/z_4_e_filetest.gno | 26 +++++++++++++ .../r/demo/boards2/z_4_f_filetest.gno | 25 ++++++++++++ .../r/demo/boards2/z_4_g_filetest.gno | 25 ++++++++++++ 15 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/z_4_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_d_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_e_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_f_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_g_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index b99e3c0b53a..a24bc02f2ec 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -32,20 +32,6 @@ func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissions return dp } -// Roles returns the list of roles. -func (dp DefaultPermissions) Roles() []Role { - var roles []Role - if dp.superRole != "" { - roles = append(roles, dp.superRole) - } - - dp.roles.Iterate("", "", func(name string, _ interface{}) bool { - roles = append(roles, Role(name)) - return false - }) - return roles -} - // RoleExists checks if a role exists. func (dp DefaultPermissions) RoleExists(r Role) bool { if dp.superRole != "" && r == dp.superRole { @@ -215,7 +201,7 @@ func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { // Admins should not be able to assign or remove the Owner role from members. caller := std.GetOrigCaller() if dp.HasRole(caller, RoleAdmin) { - role, ok := args[1].(Role) + role, ok := args[2].(Role) if !ok { panic("expected a valid member role") } @@ -223,7 +209,7 @@ func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { if role == RoleOwner { panic("admins are not allowed to promote members to Owner") } else { - member, ok := args[0].(std.Address) + member, ok := args[1].(std.Address) if !ok { panic("expected a valid member address") } diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index 33b63cdcadf..625d56720c0 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -17,11 +17,6 @@ func TestNewDefaultPermissions(t *testing.T) { perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) - urequire.Equal(t, len(roles), len(perms.Roles()), "roles") - for i, r := range perms.Roles() { - uassert.Equal(t, string(roles[i]), string(r)) - } - for _, r := range roles { uassert.True(t, perms.RoleExists(r)) } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index b21bf1555cb..d99f8b84aa4 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -212,37 +212,44 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { reply.Update(title, body) } -func InviteMember(user std.Address, role Role) { +func InviteMember(bid BoardID, user std.Address, role Role) { assertIsUserCall() + perms := mustGetPermissions(bid) caller := std.GetOrigCaller() args := Args{user, role} - gPerm.WithPermission(caller, PermissionMemberInvite, args, func(Args) { - if err := gPerm.AddUser(user, role); err != nil { + perms.WithPermission(caller, PermissionMemberInvite, args, func(Args) { + if err := perms.AddUser(user, role); err != nil { panic(err) } }) } -func RemoveMember(user std.Address) { +func RemoveMember(bid BoardID, user std.Address) { assertIsUserCall() + perms := mustGetPermissions(bid) caller := std.GetOrigCaller() - gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { - if !gPerm.RemoveUser(user) { + perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { + if !perms.RemoveUser(user) { panic("member not found") } }) } -// TODO: Allow changing board user roles -func ChangeMemberRole(member std.Address, role Role) { +func HasMemberRole(bid BoardID, member std.Address, role Role) bool { + perms := mustGetPermissions(bid) + return perms.HasRole(member, role) +} + +func ChangeMemberRole(bid BoardID, member std.Address, role Role) { assertIsUserCall() + perms := mustGetPermissions(bid) caller := std.GetOrigCaller() - args := Args{member, role} - gPerm.WithPermission(caller, PermissionRoleChange, args, func(Args) { - if err := gPerm.SetUserRoles(member, role); err != nil { + args := Args{bid, member, role} + perms.WithPermission(caller, PermissionRoleChange, args, func(Args) { + if err := perms.SetUserRoles(member, role); err != nil { panic(err) } }) @@ -307,3 +314,11 @@ func assertReplyVisible(thread *Post) { panic("reply with ID: " + thread.GetPostID().String() + " was hidden") } } + +func mustGetPermissions(bid BoardID) Permissions { + if bid != 0 { + board := mustGetBoard(bid) + return board.perms + } + return gPerm +} diff --git a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno index 6497b4203e1..3218334419d 100644 --- a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno @@ -9,6 +9,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { @@ -16,7 +17,7 @@ func init() { } func main() { - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) println("ok") } diff --git a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno index eac31e48707..01bdac4cf75 100644 --- a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno @@ -10,19 +10,20 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { // Add an admin user std.TestSetOrigCaller(owner) - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin user std.TestSetOrigCaller(admin) } func main() { - boards2.InviteMember(user, boards2.RoleOwner) + boards2.InviteMember(bid, user, boards2.RoleOwner) } // Error: diff --git a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno index 01ca55cb2f2..3151bf519de 100644 --- a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno @@ -10,19 +10,20 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { // Add an admin user std.TestSetOrigCaller(owner) - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin user std.TestSetOrigCaller(admin) } func main() { - boards2.InviteMember(user, boards2.RoleAdmin) + boards2.InviteMember(bid, user, boards2.RoleAdmin) println("ok") } diff --git a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno index bd1a3dafe08..23f9bc7d3a6 100644 --- a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno @@ -12,6 +12,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo" newName = "barbaz" ) @@ -21,7 +22,7 @@ func init() { // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(member, boards2.RoleOwner) + boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") diff --git a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno index 30617a49e52..f953f14a1e4 100644 --- a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno @@ -13,6 +13,7 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo" newName = "barbaz" ) @@ -22,12 +23,12 @@ func init() { // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(member, boards2.RoleOwner) + boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") // Invite a new member that doesn't own the user that matches the new board name - boards2.InviteMember(member2, boards2.RoleOwner) + boards2.InviteMember(bid, member2, boards2.RoleOwner) std.TestSetOrigCaller(member2) boards2.CreateBoard(name) diff --git a/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno new file mode 100644 index 00000000000..e0a1a65a92e --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno @@ -0,0 +1,36 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + initialRole = boards2.RoleGuest + newRole = boards2.RoleAdmin + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, member, boards2.RoleGuest) +} + +func main() { + if boards2.HasMemberRole(bid, member, initialRole) { + println("ok") + } + + boards2.ChangeMemberRole(bid, member, newRole) + + if boards2.HasMemberRole(bid, member, newRole) { + println("ok") + } +} + +// Output: +// ok +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno new file mode 100644 index 00000000000..dd638576459 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno @@ -0,0 +1,38 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + initialRole = boards2.RoleGuest + newRole = boards2.RoleAdmin +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("foo") // Operate on board DAO members + boards2.InviteMember(bid, member, boards2.RoleGuest) +} + +func main() { + if boards2.HasMemberRole(bid, member, initialRole) { + println("ok") + } + + boards2.ChangeMemberRole(bid, member, newRole) + + if boards2.HasMemberRole(bid, member, newRole) { + println("ok") + } +} + +// Output: +// ok +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno new file mode 100644 index 00000000000..514233d8e06 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + owner2 = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + admin = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, owner2, boards2.RoleOwner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) + + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.ChangeMemberRole(bid, owner2, boards2.RoleAdmin) +} + +// Error: +// admins are not allowed to remove the Owner role diff --git a/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno new file mode 100644 index 00000000000..d589fe9e694 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + admin2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin2, boards2.RoleAdmin) + + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.ChangeMemberRole(bid, admin2, boards2.RoleOwner) +} + +// Error: +// admins are not allowed to promote members to Owner diff --git a/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno new file mode 100644 index 00000000000..449cc6c06a1 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) +} + +func main() { + boards2.ChangeMemberRole(bid, admin, boards2.RoleOwner) // Owner can promote other members to Owner + println("ok") +} + +// Output: +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno new file mode 100644 index 00000000000..c6622b88c8a --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleGuest) +} + +func main() { + boards2.ChangeMemberRole(bid, admin, boards2.Role("foo")) +} + +// Error: +// invalid role: foo diff --git a/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno new file mode 100644 index 00000000000..24fec3fb9e3 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleGuest) +} + +func main() { + boards2.ChangeMemberRole(bid, "invalid address", boards2.RoleModerator) +} + +// Error: +// user not found