From 2124e8497d8b09c216fa79260c699e1c516317da Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 12:30:11 +0100 Subject: [PATCH 01/18] feat: assert caller fee when creating boards Caller must have a user registered in the users realm or pay a fee to create new boards. Changeset also renames `gAuth` to `gPerm`. --- examples/gno.land/r/demo/boards2/boards.gno | 8 ++--- examples/gno.land/r/demo/boards2/public.gno | 39 ++++++++------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 752f63bc438..73b3c6adb5b 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -7,11 +7,11 @@ import ( "gno.land/p/demo/boards2/admindao" ) -// Default minimum fee in ugnot required for anonymous users -const defaultAnonymousFee = 100_000_000 +// Default board creation fee in ugnot required for anonymous users +const defaultAnonymousFee = 20 * 1_000_000 var ( - gAuth Permissioner + gPerm Permissioner gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board @@ -22,7 +22,7 @@ func init() { admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 // TODO: Implement support for assigning a new realm DAO dao := admindao.New(admindao.WithMember(admin)) - gAuth = NewACL( + gPerm = NewACL( dao, WithSuperRole(RoleOwner), // TODO: Assign roles and permissions diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index e3cb3066f3d..fe80b34aa96 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -23,14 +23,12 @@ func CreateBoard(name string) BoardID { panic("board name is empty") } - // TODO: Now that registered user requirement is removed must define a way to avoid - // increasing the IDs. Require a fee? - // Or we have to change the way boards are created, which could be async. - caller := std.GetOrigCaller() + assertAnonymousCallerFeeReceived(caller) + id := incGetBoardID() args := Args{name, id} - gAuth.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { + gPerm.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { // TODO: Do the callback really need the args or we could have the same result directly referencing? name := a[0].(string) id := a[1].(BoardID) @@ -44,13 +42,11 @@ func CreateBoard(name string) BoardID { func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() + // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() assertHasPermission(caller, PermissionThreadCreate) // TODO: Who can create threads? - assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? assertBoardExists(bid) - // TODO: Assert that caller is a board member (when board type is invite only) - board := mustGetBoard(bid) thread := board.AddThread(caller, title, body) return thread.id @@ -59,9 +55,8 @@ 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) caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? - board := mustGetBoard(bid) thread := mustGetThread(board, threadID) @@ -83,8 +78,8 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { assertIsUserCall() + // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) assertBoardExists(dstBoardID) board := mustGetBoard(bid) @@ -111,7 +106,7 @@ func DeleteThread(bid BoardID, threadID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID} - gAuth.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { + gPerm.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -132,7 +127,7 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID, replyID} - gAuth.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { + gPerm.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -152,7 +147,7 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { caller := std.GetOrigCaller() args := Args{bid, threadID, title, body} - gAuth.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { + gPerm.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -186,22 +181,18 @@ func assertIsUserCall() { } } -func assertAnonymousFeeReceived() { - sent := std.GetOrigSend() - fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) - if len(sent) == 0 || sent[0].IsLT(fee) { - panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") - } -} - func assertAnonymousCallerFeeReceived(caller std.Address) { if users.GetUserByAddress(caller) == nil { - assertAnonymousFeeReceived() + sent := std.GetOrigSend() + fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) + if len(sent) == 0 || sent[0].IsLT(fee) { + panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") + } } } func assertHasPermission(user std.Address, p Permission) { - if !gAuth.HasPermission(user, p) { + if !gPerm.HasPermission(user, p) { panic("unauthorized") } } From e8b0cf98f1b817dbc5a5a51cdec31f45096779b6 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 12:46:59 +0100 Subject: [PATCH 02/18] refactor: rename `ACL` to `DefaultPermissions` Renamed to follow the preferred name that has been mentioned during board requirements discussions. --- examples/gno.land/r/demo/boards2/acl.gno | 37 +++--- .../gno.land/r/demo/boards2/acl_options.gno | 30 ++--- examples/gno.land/r/demo/boards2/acl_test.gno | 110 +++++++++--------- examples/gno.land/r/demo/boards2/boards.gno | 2 +- 4 files changed, 91 insertions(+), 88 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/acl.gno b/examples/gno.land/r/demo/boards2/acl.gno index 631591e1214..5f716f3aeba 100644 --- a/examples/gno.land/r/demo/boards2/acl.gno +++ b/examples/gno.land/r/demo/boards2/acl.gno @@ -19,8 +19,8 @@ type ( // Role defines the type for user roles. Role string - // ACL or access control list manages user roles and permissions. - ACL struct { + // DefaultPermissions manages users, roles and permissions. + DefaultPermissions struct { superRole Role dao *admindao.AdminDAO users *avl.Tree // string(std.Address) -> []Role @@ -28,23 +28,24 @@ type ( } ) -// NewACL create a new access control list. -func NewACL(dao *admindao.AdminDAO, options ...ACLOption) *ACL { - acl := &ACL{ +// NewDefaultPermissions creates a new permissions type. +// This type is a default implementation to handle users, roles and permissions. +func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissionsOption) *DefaultPermissions { + dp := &DefaultPermissions{ dao: dao, roles: avl.NewTree(), users: avl.NewTree(), } for _, apply := range options { - apply(acl) + apply(dp) } - return acl + return dp } // Roles returns the list of roles. -func (acl ACL) Roles() []Role { +func (dp DefaultPermissions) Roles() []Role { var roles []Role - acl.roles.Iterate("", "", func(name string, _ interface{}) bool { + dp.roles.Iterate("", "", func(name string, _ interface{}) bool { roles = append(roles, Role(name)) return false }) @@ -52,8 +53,8 @@ func (acl ACL) Roles() []Role { } // 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()) +func (dp DefaultPermissions) GetUserRoles(user std.Address) []Role { + v, found := dp.users.Get(user.String()) if !found { return nil } @@ -61,8 +62,8 @@ func (acl ACL) GetUserRoles(user std.Address) []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) { +func (dp DefaultPermissions) HasRole(user std.Address, r Role) bool { + for _, role := range dp.GetUserRoles(user) { if role == r { return true } @@ -71,10 +72,10 @@ func (acl ACL) HasRole(user std.Address, r Role) bool { } // HasPermission checks if a user has a specific permission. -func (acl ACL) HasPermission(user std.Address, perm Permission) bool { +func (dp DefaultPermissions) 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)) + for _, r := range dp.GetUserRoles(user) { + v, found := dp.roles.Get(string(r)) if !found { continue } @@ -90,8 +91,8 @@ func (acl ACL) HasPermission(user std.Address, perm Permission) bool { // 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) { +func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, a Args, cb func(Args)) { + if !dp.HasPermission(user, perm) || !dp.dao.IsMember(user) { panic("unauthorized") } diff --git a/examples/gno.land/r/demo/boards2/acl_options.gno b/examples/gno.land/r/demo/boards2/acl_options.gno index 6a481470766..fdaef050206 100644 --- a/examples/gno.land/r/demo/boards2/acl_options.gno +++ b/examples/gno.land/r/demo/boards2/acl_options.gno @@ -2,29 +2,29 @@ package boards2 import "std" -// ACLOption configures an ACL. -type ACLOption func(*ACL) +// DefaultPermissionsOption configures an DefaultPermissions. +type DefaultPermissionsOption func(*DefaultPermissions) // WithSuperRole assigns a super role. -// A super role is one that have all ACL permissions. +// A super role is one that have all 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 +func WithSuperRole(r Role) DefaultPermissionsOption { + return func(dp *DefaultPermissions) { + dp.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...)) +// 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)] + dp.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...)) +// WithRole add a role to default permissions with one or more assigned permissions. +func WithRole(r Role, p Permission, extra ...Permission) DefaultPermissionsOption { + return func(dp *DefaultPermissions) { + dp.roles.Set(string(r), append([]Permission{p}, extra...)) } } diff --git a/examples/gno.land/r/demo/boards2/acl_test.gno b/examples/gno.land/r/demo/boards2/acl_test.gno index 2b765920a45..bf56e628c4b 100644 --- a/examples/gno.land/r/demo/boards2/acl_test.gno +++ b/examples/gno.land/r/demo/boards2/acl_test.gno @@ -9,25 +9,27 @@ import ( "gno.land/p/demo/urequire" ) -func TestNewACL(t *testing.T) { +var _ Permissioner = (*DefaultPermissions)(nil) + +func TestNewDefaultPermissions(t *testing.T) { roles := []string{"a", "b"} dao := admindao.New() - acl := NewACL(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) + perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) - urequire.Equal(t, len(roles), len(acl.Roles()), "roles") - for i, r := range acl.Roles() { + urequire.Equal(t, len(roles), len(perms.Roles()), "roles") + for i, r := range perms.Roles() { uassert.Equal(t, roles[i], string(r)) } } -func TestACLWithPermission(t *testing.T) { +func TestDefaultPermissionsWithPermission(t *testing.T) { cases := []struct { name string user std.Address permission Permission args Args - acl *ACL + perms *DefaultPermissions err string called bool }{ @@ -35,7 +37,7 @@ func TestACLWithPermission(t *testing.T) { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), @@ -47,7 +49,7 @@ func TestACLWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", args: Args{"a", "b"}, - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), @@ -58,7 +60,7 @@ func TestACLWithPermission(t *testing.T) { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), WithRole("foo", "bar"), @@ -69,7 +71,7 @@ func TestACLWithPermission(t *testing.T) { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), @@ -91,7 +93,7 @@ func TestACLWithPermission(t *testing.T) { } testCaseFn := func() { - tc.acl.WithPermission(tc.user, tc.permission, tc.args, callback) + tc.perms.WithPermission(tc.user, tc.permission, tc.args, callback) } if tc.err != "" { @@ -110,40 +112,40 @@ func TestACLWithPermission(t *testing.T) { } } -func TestACLGetUserRoles(t *testing.T) { +func TestDefaultPermissionsGetUserRoles(t *testing.T) { cases := []struct { name string user std.Address roles []string - acl *ACL + perms *DefaultPermissions }{ { name: "single role", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), }, { name: "multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin", "foo", "bar"}, - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), }, { - name: "without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + name: "without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), }, { - name: "not a user", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New()), + name: "not a user", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New()), }, { name: "multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), @@ -154,7 +156,7 @@ func TestACLGetUserRoles(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - roles := tc.acl.GetUserRoles(tc.user) + roles := tc.perms.GetUserRoles(tc.user) urequire.Equal(t, len(tc.roles), len(roles), "user role count") for i, r := range roles { @@ -164,62 +166,62 @@ func TestACLGetUserRoles(t *testing.T) { } } -func TestACLHasRole(t *testing.T) { +func TestDefaultPermissionsHasRole(t *testing.T) { cases := []struct { - name string - user std.Address - role Role - acl *ACL - want bool + name string + user std.Address + role Role + perms *DefaultPermissions + want bool }{ { - name: "ok", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "admin", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), - want: true, + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "admin", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + want: true, }, { - name: "ok with multiple roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "foo", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), - want: true, + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "foo", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), + want: true, }, { - name: "user without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + name: "user without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), }, { - name: "has no role", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "bar", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), + name: "has no role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "bar", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := tc.acl.HasRole(tc.user, tc.role) + got := tc.perms.HasRole(tc.user, tc.role) uassert.Equal(t, got, tc.want) }) } } -func TestACLHasPermission(t *testing.T) { +func TestDefaultPermissionsHasPermission(t *testing.T) { cases := []struct { name string user std.Address permission Permission - acl *ACL + perms *DefaultPermissions want bool }{ { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), @@ -230,7 +232,7 @@ func TestACLHasPermission(t *testing.T) { name: "ok with multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), @@ -242,7 +244,7 @@ func TestACLHasPermission(t *testing.T) { name: "ok with multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), WithRole("foo", "bar"), @@ -254,7 +256,7 @@ func TestACLHasPermission(t *testing.T) { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", - acl: NewACL( + perms: NewDefaultPermissions( admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), @@ -264,7 +266,7 @@ func TestACLHasPermission(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := tc.acl.HasPermission(tc.user, tc.permission) + got := tc.perms.HasPermission(tc.user, tc.permission) uassert.Equal(t, got, tc.want) }) } diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 73b3c6adb5b..df7631a7f27 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -22,7 +22,7 @@ func init() { admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 // TODO: Implement support for assigning a new realm DAO dao := admindao.New(admindao.WithMember(admin)) - gPerm = NewACL( + gPerm = NewDefaultPermissions( dao, WithSuperRole(RoleOwner), // TODO: Assign roles and permissions From 44a884cb78e02b56113d1818c2cabdca0b7e423e Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 12:53:41 +0100 Subject: [PATCH 03/18] refactor: rename `DefaultPermissions` files --- .../gno.land/r/demo/boards2/{acl.gno => default_permissions.gno} | 0 .../boards2/{acl_options.gno => default_permissions_options.gno} | 0 .../r/demo/boards2/{acl_test.gno => default_permissions_test.gno} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename examples/gno.land/r/demo/boards2/{acl.gno => default_permissions.gno} (100%) rename examples/gno.land/r/demo/boards2/{acl_options.gno => default_permissions_options.gno} (100%) rename examples/gno.land/r/demo/boards2/{acl_test.gno => default_permissions_test.gno} (100%) diff --git a/examples/gno.land/r/demo/boards2/acl.gno b/examples/gno.land/r/demo/boards2/default_permissions.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/acl.gno rename to examples/gno.land/r/demo/boards2/default_permissions.gno diff --git a/examples/gno.land/r/demo/boards2/acl_options.gno b/examples/gno.land/r/demo/boards2/default_permissions_options.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/acl_options.gno rename to examples/gno.land/r/demo/boards2/default_permissions_options.gno diff --git a/examples/gno.land/r/demo/boards2/acl_test.gno b/examples/gno.land/r/demo/boards2/default_permissions_test.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/acl_test.gno rename to examples/gno.land/r/demo/boards2/default_permissions_test.gno From 66efcbf725435453d4f00f769016958ba5128ab7 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 13:23:33 +0100 Subject: [PATCH 04/18] feat: move board name check to public realm function Board creation function should not check if the name is duplicated. --- examples/gno.land/r/demo/boards2/board.gno | 4 ---- examples/gno.land/r/demo/boards2/public.gno | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 64cc8730d6b..ee1b548aa0f 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -31,10 +31,6 @@ type Board struct { } func newBoard(id BoardID, name string, creator std.Address) *Board { - if gBoardsByName.Has(name) { - panic("board already exists") - } - return &Board{ id: id, name: name, diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index fe80b34aa96..a08fa8e3759 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -29,8 +29,12 @@ func CreateBoard(name string) BoardID { id := incGetBoardID() args := Args{name, id} gPerm.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { - // TODO: Do the callback really need the args or we could have the same result directly referencing? name := a[0].(string) + if gBoardsByName.Has(name) { + panic("board already exists") + } + + // TODO: Do the callback really need the args or we could have the same result directly referencing? id := a[1].(BoardID) board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) From 59508cc24b38efacff83c6b0ef21ca914d02826a Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 15:23:59 +0100 Subject: [PATCH 05/18] feat: initial approach to handle business logic for permissions --- examples/gno.land/r/demo/boards2/boards.gno | 16 ++---- .../r/demo/boards2/default_permissions.gno | 52 +++++++++++++++---- .../boards2/default_permissions_handlers.gno | 0 3 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/default_permissions_handlers.gno diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index df7631a7f27..ca6586ea30c 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -11,25 +11,15 @@ import ( const defaultAnonymousFee = 20 * 1_000_000 var ( - gPerm Permissioner + gPerm Permissioner // TODO: Support changing the permissioner gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board ) func init() { - // TODO: Decide how to initialize realm owner (DAO owner member) - admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - // TODO: Implement support for assigning a new realm DAO - dao := admindao.New(admindao.WithMember(admin)) - gPerm = NewDefaultPermissions( - dao, - WithSuperRole(RoleOwner), - // TODO: Assign roles and permissions - // WithRole(RoleAdmin, permissions...), - // WithRole(RoleModerator, permissions...), - WithUser(admin, RoleOwner), - ) + // Initialize the default realm permissions + gPerm = createDefaultPermissions() } // incGetBoardID returns a new board ID. diff --git a/examples/gno.land/r/demo/boards2/default_permissions.gno b/examples/gno.land/r/demo/boards2/default_permissions.gno index 5f716f3aeba..e4c95ee5b00 100644 --- a/examples/gno.land/r/demo/boards2/default_permissions.gno +++ b/examples/gno.land/r/demo/boards2/default_permissions.gno @@ -7,8 +7,6 @@ import ( "gno.land/p/demo/boards2/admindao" ) -// TODO: Support to deal with permissions for anonymous users? - const ( RoleOwner Role = "owner" RoleAdmin = "admin" @@ -16,6 +14,11 @@ const ( ) type ( + // PermissionsHandlerFunc defines a function to handle permission callbacks. + // Handlers are called by the `WithPermission()` method to execute callbacks + // when users have the permission assigned. + PermissionsHandlerFunc func(Args, *admindao.AdminDAO, func(Args)) error + // Role defines the type for user roles. Role string @@ -23,6 +26,7 @@ type ( DefaultPermissions struct { superRole Role dao *admindao.AdminDAO + handlers *avl.Tree // string(permission) -> PermissionsHandlerFunc users *avl.Tree // string(std.Address) -> []Role roles *avl.Tree // string(role) -> []Permission } @@ -32,9 +36,10 @@ type ( // This type is a default implementation to handle users, roles and permissions. func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissionsOption) *DefaultPermissions { dp := &DefaultPermissions{ - dao: dao, - roles: avl.NewTree(), - users: avl.NewTree(), + dao: dao, + handlers: avl.NewTree(), + roles: avl.NewTree(), + users: avl.NewTree(), } for _, apply := range options { apply(dp) @@ -89,13 +94,42 @@ func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bo return false } +// HandleFunc registers a handler function for a permission. +func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc) { + dp.handlers.Set(string(p), fn) +} + // WithPermission calls a callback when a user has a specific permission. -// It panics on error. -func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, a Args, cb func(Args)) { +// It panics on error or when a handler fails. +// Callbacks are by default called when there is no handle registered for the permission. +func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, args Args, cb func(Args)) { if !dp.HasPermission(user, perm) || !dp.dao.IsMember(user) { panic("unauthorized") } - // TODO: Support DAO proposals that run the callback on proposal execution - cb(a) + h, found := dp.handlers.Get(string(perm)) + if !found { + cb(args) // TODO: Should we fail instead? + return + } + + fn := h.(PermissionsHandlerFunc) + if err := fn(args, dp.dao, cb); err != nil { + panic(err) + } +} + +func createDefaultPermissions() *DefaultPermissions { + // TODO: Define and change the default realm owner (or owners) + admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + dao := admindao.New(admindao.WithMember(admin)) + perms := NewDefaultPermissions( + dao, + WithSuperRole(RoleOwner), + // TODO: Assign roles and permissions + // WithRole(RoleAdmin, permissions...), + // WithRole(RoleModerator, permissions...), + WithUser(admin, RoleOwner), + ) + return perms } diff --git a/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno b/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno new file mode 100644 index 00000000000..e69de29bb2d From 4e659676361c800679cd5078dde3cd06647ffa82 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 16:51:18 +0100 Subject: [PATCH 06/18] feat: add board creation permission handler This handler deals with permissioner checks for board creation using the `WithPermission` function. --- examples/gno.land/r/demo/boards2/boards.gno | 3 -- .../r/demo/boards2/default_permissions.gno | 17 ++++---- .../boards2/default_permissions_handlers.gno | 40 +++++++++++++++++++ examples/gno.land/r/demo/boards2/public.gno | 16 +------- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index ca6586ea30c..f8674cf6161 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -7,9 +7,6 @@ import ( "gno.land/p/demo/boards2/admindao" ) -// Default board creation fee in ugnot required for anonymous users -const defaultAnonymousFee = 20 * 1_000_000 - var ( gPerm Permissioner // TODO: Support changing the permissioner gLastBoardID BoardID diff --git a/examples/gno.land/r/demo/boards2/default_permissions.gno b/examples/gno.land/r/demo/boards2/default_permissions.gno index e4c95ee5b00..220ad69b141 100644 --- a/examples/gno.land/r/demo/boards2/default_permissions.gno +++ b/examples/gno.land/r/demo/boards2/default_permissions.gno @@ -17,7 +17,7 @@ type ( // PermissionsHandlerFunc defines a function to handle permission callbacks. // Handlers are called by the `WithPermission()` method to execute callbacks // when users have the permission assigned. - PermissionsHandlerFunc func(Args, *admindao.AdminDAO, func(Args)) error + PermissionsHandlerFunc func(Args, *admindao.AdminDAO, func(Args)) // Role defines the type for user roles. Role string @@ -100,7 +100,7 @@ func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc } // WithPermission calls a callback when a user has a specific permission. -// It panics on error or when a handler fails. +// It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, args Args, cb func(Args)) { if !dp.HasPermission(user, perm) || !dp.dao.IsMember(user) { @@ -114,22 +114,25 @@ func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, a } fn := h.(PermissionsHandlerFunc) - if err := fn(args, dp.dao, cb); err != nil { - panic(err) - } + fn(args, dp.dao, cb) } func createDefaultPermissions() *DefaultPermissions { // TODO: Define and change the default realm owner (or owners) admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + // 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 `Permissioner`?? dao := admindao.New(admindao.WithMember(admin)) perms := NewDefaultPermissions( dao, WithSuperRole(RoleOwner), - // TODO: Assign roles and permissions - // WithRole(RoleAdmin, permissions...), + WithRole(RoleAdmin, PermissionBoardCreate), + // TODO: Finish assigning all roles and permissions // WithRole(RoleModerator, permissions...), WithUser(admin, RoleOwner), ) + + perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) + return perms } diff --git a/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno b/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno index e69de29bb2d..17badf9be65 100644 --- a/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno +++ b/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno @@ -0,0 +1,40 @@ +package boards2 + +import ( + "errors" + "std" + + "gno.land/p/demo/boards2/admindao" + "gno.land/r/demo/users" +) + +// Default board creation fee in ugnot required for anonymous users +const defaultAnonymousFee = 20 * 1_000_000 // TODO: Allow changing the fee though a proposal + +func handleBoardCreate(args Args, _ *admindao.AdminDAO, cb func(Args)) { + // TODO: This way of dealing with arguments is delicate, ideally types should be used + name := args[0].(string) + if std.Address(name).IsValid() { + panic("addresses are not allowed as board name") + } + + // When the board name is the name of a registered user + // check that caller is the owner of the name. + caller := std.GetOrigCaller() + user := users.GetUserByName(name) + if user != nil && user.Address != caller { + panic("board name is a user name registered to a different user") + } + + // Try to get the user by address or otheriwse consider the caller an anonymous user. + // Anonymous users must pay a fee to create boards. + if user == nil && users.GetUserByAddress(caller) == nil { + sent := std.GetOrigSend() + fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) + if len(sent) == 0 || sent[0].IsLT(fee) { + panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") + } + } + + cb(args) +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index a08fa8e3759..c63b9f0d190 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -3,8 +3,6 @@ package boards2 import ( "std" "strings" - - "gno.land/r/demo/users" ) func GetBoardIDFromName(name string) (BoardID, bool) { @@ -24,17 +22,15 @@ func CreateBoard(name string) BoardID { } caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) - id := incGetBoardID() args := Args{name, id} gPerm.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { + // TODO: Do the callback really need the args or we could have the same result directly referencing? name := a[0].(string) if gBoardsByName.Has(name) { panic("board already exists") } - // TODO: Do the callback really need the args or we could have the same result directly referencing? id := a[1].(BoardID) board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) @@ -185,16 +181,6 @@ func assertIsUserCall() { } } -func assertAnonymousCallerFeeReceived(caller std.Address) { - if users.GetUserByAddress(caller) == nil { - sent := std.GetOrigSend() - fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) - if len(sent) == 0 || sent[0].IsLT(fee) { - panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") - } - } -} - func assertHasPermission(user std.Address, p Permission) { if !gPerm.HasPermission(user, p) { panic("unauthorized") From 8acd659ea52002347bea4f78f3fb3735e05c3439 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 16:57:13 +0100 Subject: [PATCH 07/18] refactor: rename default permission files --- examples/gno.land/r/demo/boards2/permission.gno | 8 +------- .../{default_permissions.gno => permission_default.gno} | 0 ...t_permissions_handlers.gno => permission_handlers.gno} | 1 - ...ult_permissions_options.gno => permission_options.gno} | 0 .../{default_permissions_test.gno => permission_test.gno} | 0 5 files changed, 1 insertion(+), 8 deletions(-) rename examples/gno.land/r/demo/boards2/{default_permissions.gno => permission_default.gno} (100%) rename examples/gno.land/r/demo/boards2/{default_permissions_handlers.gno => permission_handlers.gno} (99%) rename examples/gno.land/r/demo/boards2/{default_permissions_options.gno => permission_options.gno} (100%) rename examples/gno.land/r/demo/boards2/{default_permissions_test.gno => permission_test.gno} (100%) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 70ce929c38d..5debda5b1a1 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -1,9 +1,6 @@ package boards2 -import ( - "errors" - "std" -) +import "std" const ( PermissionBoardCreate Permission = "board:create" @@ -14,9 +11,6 @@ const ( PermissionReplyDelete = "reply:delete" ) -// ErrUnauzorized indicates that user doesn't have a required permission. -var ErrUnauzorized = errors.New("unauthorized") - type ( // Permission defines the type for permissions. Permission string diff --git a/examples/gno.land/r/demo/boards2/default_permissions.gno b/examples/gno.land/r/demo/boards2/permission_default.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/default_permissions.gno rename to examples/gno.land/r/demo/boards2/permission_default.gno diff --git a/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno similarity index 99% rename from examples/gno.land/r/demo/boards2/default_permissions_handlers.gno rename to examples/gno.land/r/demo/boards2/permission_handlers.gno index 17badf9be65..7c68f514abc 100644 --- a/examples/gno.land/r/demo/boards2/default_permissions_handlers.gno +++ b/examples/gno.land/r/demo/boards2/permission_handlers.gno @@ -1,7 +1,6 @@ package boards2 import ( - "errors" "std" "gno.land/p/demo/boards2/admindao" diff --git a/examples/gno.land/r/demo/boards2/default_permissions_options.gno b/examples/gno.land/r/demo/boards2/permission_options.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/default_permissions_options.gno rename to examples/gno.land/r/demo/boards2/permission_options.gno diff --git a/examples/gno.land/r/demo/boards2/default_permissions_test.gno b/examples/gno.land/r/demo/boards2/permission_test.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/default_permissions_test.gno rename to examples/gno.land/r/demo/boards2/permission_test.gno From 4602229d4f43de7e5d858f53ed733069901ab595 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 17:48:10 +0100 Subject: [PATCH 08/18] test: add board creation file tests --- .../r/demo/boards2/permission_default.gno | 10 +++++--- .../gno.land/r/demo/boards2/z_0a_filetest.gno | 21 ++++++++++++++++ .../gno.land/r/demo/boards2/z_0b_filetest.gno | 20 ++++++++++++++++ .../gno.land/r/demo/boards2/z_0c_filetest.gno | 24 +++++++++++++++++++ .../gno.land/r/demo/boards2/z_0d_filetest.gno | 11 +++++++++ .../gno.land/r/demo/boards2/z_0e_filetest.gno | 20 ++++++++++++++++ .../gno.land/r/demo/boards2/z_0f_filetest.gno | 20 ++++++++++++++++ 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/z_0a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0d_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0e_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0f_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 220ad69b141..1c5d3604a2d 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -80,6 +80,10 @@ func (dp DefaultPermissions) HasRole(user std.Address, r Role) bool { func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bool { // TODO: Should we check that the user belongs to the DAO? for _, r := range dp.GetUserRoles(user) { + if dp.superRole == r { + return true + } + v, found := dp.roles.Get(string(r)) if !found { continue @@ -119,17 +123,17 @@ func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, a func createDefaultPermissions() *DefaultPermissions { // TODO: Define and change the default realm owner (or owners) - admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 // 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 `Permissioner`?? - dao := admindao.New(admindao.WithMember(admin)) + dao := admindao.New(admindao.WithMember(owner)) perms := NewDefaultPermissions( dao, WithSuperRole(RoleOwner), WithRole(RoleAdmin, PermissionBoardCreate), // TODO: Finish assigning all roles and permissions // WithRole(RoleModerator, permissions...), - WithUser(admin, RoleOwner), + WithUser(owner, RoleOwner), ) perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) diff --git a/examples/gno.land/r/demo/boards2/z_0a_filetest.gno b/examples/gno.land/r/demo/boards2/z_0a_filetest.gno new file mode 100644 index 00000000000..ea270767001 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0a_filetest.gno @@ -0,0 +1,21 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(admin) +} + +func main() { + bid := boards2.CreateBoard("test1") + println("ID =", bid) +} + +// Output: +// ID = 1 diff --git a/examples/gno.land/r/demo/boards2/z_0b_filetest.gno b/examples/gno.land/r/demo/boards2/z_0b_filetest.gno new file mode 100644 index 00000000000..1c24e10555f --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.CreateBoard("") +} + +// Error: +// board name is empty diff --git a/examples/gno.land/r/demo/boards2/z_0c_filetest.gno b/examples/gno.land/r/demo/boards2/z_0c_filetest.gno new file mode 100644 index 00000000000..6a7c13c99cc --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0c_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + boardName = "test1" +) + +func init() { + std.TestSetOrigCaller(admin) + boards2.CreateBoard(boardName) +} + +func main() { + boards2.CreateBoard(boardName) +} + +// Error: +// board already exists diff --git a/examples/gno.land/r/demo/boards2/z_0d_filetest.gno b/examples/gno.land/r/demo/boards2/z_0d_filetest.gno new file mode 100644 index 00000000000..8992a5cb133 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0d_filetest.gno @@ -0,0 +1,11 @@ +// PKGPATH: gno.land/r/demo/boards2_test +package boards2_test + +import "gno.land/r/demo/boards2" + +func main() { + boards2.CreateBoard("foo") +} + +// Error: +// invalid non-user call diff --git a/examples/gno.land/r/demo/boards2/z_0e_filetest.gno b/examples/gno.land/r/demo/boards2/z_0e_filetest.gno new file mode 100644 index 00000000000..c3032914822 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0e_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.CreateBoard("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +} + +// Error: +// addresses are not allowed as board name diff --git a/examples/gno.land/r/demo/boards2/z_0f_filetest.gno b/examples/gno.land/r/demo/boards2/z_0f_filetest.gno new file mode 100644 index 00000000000..1ff5ad840f4 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0f_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.CreateBoard("gnoland") +} + +// Error: +// board name is a user name registered to a different user From efb2937bd7cbc3d6b716342abc7c15c8a5f96917 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 17:53:58 +0100 Subject: [PATCH 09/18] chore: remove fee requirement to create board as anonymous user At this point we deal with whitelisted users that are member of the boards realm DAO when creating boards. Anonymous users can't create boards at this point. --- .../gno.land/r/demo/boards2/permission_handlers.gno | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/permission_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno index 7c68f514abc..f012af4f8ac 100644 --- a/examples/gno.land/r/demo/boards2/permission_handlers.gno +++ b/examples/gno.land/r/demo/boards2/permission_handlers.gno @@ -7,9 +7,6 @@ import ( "gno.land/r/demo/users" ) -// Default board creation fee in ugnot required for anonymous users -const defaultAnonymousFee = 20 * 1_000_000 // TODO: Allow changing the fee though a proposal - func handleBoardCreate(args Args, _ *admindao.AdminDAO, cb func(Args)) { // TODO: This way of dealing with arguments is delicate, ideally types should be used name := args[0].(string) @@ -25,15 +22,5 @@ func handleBoardCreate(args Args, _ *admindao.AdminDAO, cb func(Args)) { panic("board name is a user name registered to a different user") } - // Try to get the user by address or otheriwse consider the caller an anonymous user. - // Anonymous users must pay a fee to create boards. - if user == nil && users.GetUserByAddress(caller) == nil { - sent := std.GetOrigSend() - fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) - if len(sent) == 0 || sent[0].IsLT(fee) { - panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") - } - } - cb(args) } From d9b48c250e722583e150b5d640c41a64dcc5f9b7 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 26 Nov 2024 18:01:05 +0100 Subject: [PATCH 10/18] fix: remove unused imports --- examples/gno.land/r/demo/boards2/boards.gno | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index f8674cf6161..53072e37542 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,11 +1,6 @@ package boards2 -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/boards2/admindao" -) +import "gno.land/p/demo/avl" var ( gPerm Permissioner // TODO: Support changing the permissioner From a948d39beb66010d080947da5198762e9be32480 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 14:32:17 +0100 Subject: [PATCH 11/18] feat: add support to add members to AdminDAO --- .../gno.land/p/demo/boards2/admindao/admindao.gno | 13 +++++++++++++ .../p/demo/boards2/admindao/admindao_test.gno | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao.gno b/examples/gno.land/p/demo/boards2/admindao/admindao.gno index 4e1d49e435e..7f7531bc133 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao.gno @@ -1,6 +1,7 @@ package admindao import ( + "errors" "std" "gno.land/p/demo/avl" @@ -9,6 +10,9 @@ import ( // TODO: Add support for proposals // TODO: Add support for events +// ErrMemberExists indicates that a member is already part of the DAO. +var ErrMemberExists = errors.New("member already exist") + // AdminDAO defines a Boards administration DAO. type AdminDAO struct { parent *AdminDAO @@ -40,6 +44,15 @@ func (dao AdminDAO) Members() []std.Address { return members } +// AddMember adds a new member to the DAO. +func (dao *AdminDAO) AddMember(user std.Address) error { + if dao.IsMember(user) { + return ErrMemberExists + } + dao.members.Set(user.String(), struct{}{}) + return nil +} + // 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()) diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno index 171f5618892..44734ca24bf 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno @@ -59,6 +59,19 @@ func TestNew(t *testing.T) { } } +func TestAdminDAOAddMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")) + + err := dao.AddMember(member) + urequire.NoError(t, err) + uassert.Equal(t, 2, len(dao.Members())) + uassert.True(t, dao.IsMember(member)) + + err = dao.AddMember(member) + uassert.ErrorIs(t, err, ErrMemberExists) +} + func TestAdminDAOIsMember(t *testing.T) { cases := []struct { name string From f4b3a0f202ea0aa9f5f2157f4367b78a3125f1ad Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 14:32:37 +0100 Subject: [PATCH 12/18] feat: add support to invite board realm members --- .../gno.land/r/demo/boards2/permission.gno | 13 +++ .../r/demo/boards2/permission_default.gno | 37 +++++-- .../r/demo/boards2/permission_test.gno | 96 ++++++++++++++++++- examples/gno.land/r/demo/boards2/public.gno | 14 +++ 4 files changed, 149 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 5debda5b1a1..3497a6ae534 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -9,12 +9,22 @@ const ( PermissionThreadDelete = "thread:delete" PermissionThreadRepost = "thread:repost" PermissionReplyDelete = "reply:delete" + PermissionMemberInvite = "member:invite" +) + +const ( + RoleOwner Role = "owner" + RoleAdmin = "admin" + RoleModerator = "moderator" ) type ( // Permission defines the type for permissions. Permission string + // Role defines the type for user roles. + Role string + // Args is a list of generic arguments. Args []interface{} @@ -26,5 +36,8 @@ type ( // WithPermission calls a callback when a user has a specific permission. // It panics on error. WithPermission(std.Address, Permission, Args, func(Args)) + + // AddUser adds a new user to the permissioner. + AddUser(std.Address, ...Role) error } ) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index 1c5d3604a2d..c1f086a0fd9 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -1,27 +1,19 @@ package boards2 import ( + "errors" "std" "gno.land/p/demo/avl" "gno.land/p/demo/boards2/admindao" ) -const ( - RoleOwner Role = "owner" - RoleAdmin = "admin" - RoleModerator = "moderator" -) - type ( // PermissionsHandlerFunc defines a function to handle permission callbacks. // Handlers are called by the `WithPermission()` method to execute callbacks // when users have the permission assigned. PermissionsHandlerFunc func(Args, *admindao.AdminDAO, func(Args)) - // Role defines the type for user roles. - Role string - // DefaultPermissions manages users, roles and permissions. DefaultPermissions struct { superRole Role @@ -57,6 +49,13 @@ func (dp DefaultPermissions) Roles() []Role { return roles } +// RoleExists checks if a role exists. +func (dp DefaultPermissions) RoleExists(r Role) bool { + return dp.roles.Iterate("", "", func(name string, _ interface{}) bool { + return Role(name) == r + }) +} + // GetUserRoles returns the list of roles assigned to a user. func (dp DefaultPermissions) GetUserRoles(user std.Address) []Role { v, found := dp.users.Get(user.String()) @@ -98,6 +97,26 @@ func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bo return false } +// AddUser adds a new user to the permissioner. +func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { + if dp.users.Has(user.String()) { + return errors.New("user already exists") + } + + 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 +} + // HandleFunc registers a handler function for a permission. func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc) { dp.handlers.Set(string(p), fn) diff --git a/examples/gno.land/r/demo/boards2/permission_test.gno b/examples/gno.land/r/demo/boards2/permission_test.gno index bf56e628c4b..858c140eda5 100644 --- a/examples/gno.land/r/demo/boards2/permission_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_test.gno @@ -12,14 +12,18 @@ import ( var _ Permissioner = (*DefaultPermissions)(nil) func TestNewDefaultPermissions(t *testing.T) { - roles := []string{"a", "b"} + roles := []Role{"a", "b"} dao := admindao.New() 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, roles[i], string(r)) + uassert.Equal(t, string(roles[i]), string(r)) + } + + for _, r := range roles { + uassert.True(t, perms.RoleExists(r)) } } @@ -271,3 +275,91 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { }) } } + +func TestDefaultPermissionsAddUser(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []Role + setup func() *DefaultPermissions + err string + }{ + { + name: "single user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "b"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + ) + }, + }, + { + name: "multiple users", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a"), + WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), + ) + }, + }, + { + name: "duplicated user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ) + }, + err: "user already exists", + }, + { + name: "duplicated user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "foo"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New(), WithRole("a", "permission1")) + }, + err: "invalid role: foo", + }, + { + name: "already a DAO member", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + ) + }, + err: "member already exist", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + + err := perm.AddUser(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)) + } + }) + } +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index c63b9f0d190..87c3675f8dd 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -175,6 +175,20 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { reply.Update(title, body) } +func InviteMember(user std.Address, role Role) { // TODO: Implement suport for removing members + assertIsUserCall() + + caller := std.GetOrigCaller() + args := Args{user, role} + gPerm.WithPermission(caller, PermissionMemberInvite, args, func(a Args) { + user := a[0].(std.Address) + role := a[1].(Role) + if err := gPerm.AddUser(user, role); err != nil { + panic(err) + } + }) +} + func assertIsUserCall() { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") From 13942ae5b9caab1313c4f4c1bdfb1ee6ab487217 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 14:52:30 +0100 Subject: [PATCH 13/18] feat: add support to remove AdminDAO members --- .../gno.land/p/demo/boards2/admindao/admindao.gno | 6 ++++++ .../p/demo/boards2/admindao/admindao_test.gno | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao.gno b/examples/gno.land/p/demo/boards2/admindao/admindao.gno index 7f7531bc133..404bfe78db7 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao.gno @@ -53,6 +53,12 @@ func (dao *AdminDAO) AddMember(user std.Address) error { return nil } +// RemoveMember removes a member from the DAO. +func (dao *AdminDAO) RemoveMember(user std.Address) (removed bool) { + _, removed = dao.members.Remove(user.String()) + return removed +} + // 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()) diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno index 44734ca24bf..999ecdfcd0a 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno @@ -72,6 +72,17 @@ func TestAdminDAOAddMember(t *testing.T) { uassert.ErrorIs(t, err, ErrMemberExists) } +func TestAdminDAORemoveMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember(member)) + + removed := dao.RemoveMember(member) + urequire.True(t, removed) + + removed = dao.RemoveMember(member) + urequire.False(t, removed) +} + func TestAdminDAOIsMember(t *testing.T) { cases := []struct { name string From 4d911c084b9787bd18bd6edf3a1d45c0eab648ca Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 14:59:49 +0100 Subject: [PATCH 14/18] feat: add support to remove board realm members --- .../gno.land/r/demo/boards2/permission.gno | 4 +++ .../r/demo/boards2/permission_default.gno | 9 ++++- .../r/demo/boards2/permission_test.gno | 36 +++++++++++++++++++ examples/gno.land/r/demo/boards2/public.gno | 14 +++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 3497a6ae534..e0cf44b814d 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -10,6 +10,7 @@ const ( PermissionThreadRepost = "thread:repost" PermissionReplyDelete = "reply:delete" PermissionMemberInvite = "member:invite" + PermissionMemberRemove = "member:remove" ) const ( @@ -39,5 +40,8 @@ type ( // AddUser adds a new user to the permissioner. AddUser(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 c1f086a0fd9..ab4ea521f11 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -97,7 +97,7 @@ func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bo return false } -// AddUser adds a new user to the permissioner. +// AddUser adds a new user to permissions. func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { if dp.users.Has(user.String()) { return errors.New("user already exists") @@ -117,6 +117,13 @@ func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { return nil } +// RemoveUser removes a user from permissions. +func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { + _, removed := dp.users.Remove(user.String()) + dp.dao.RemoveMember(user) + return removed +} + // HandleFunc registers a handler function for a permission. func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc) { dp.handlers.Set(string(p), fn) diff --git a/examples/gno.land/r/demo/boards2/permission_test.gno b/examples/gno.land/r/demo/boards2/permission_test.gno index 858c140eda5..bf64018c11a 100644 --- a/examples/gno.land/r/demo/boards2/permission_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_test.gno @@ -363,3 +363,39 @@ func TestDefaultPermissionsAddUser(t *testing.T) { }) } } + +func TestDefaultPermissionsRemoveUser(t *testing.T) { + cases := []struct { + name string + user std.Address + setup func() *DefaultPermissions + want bool + }{ + { + name: "ok", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ) + }, + want: true, + }, + { + name: "user not found", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New()) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + got := perm.RemoveUser(tc.user) + uassert.Equal(t, tc.want, got) + }) + } +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 87c3675f8dd..9820c45499f 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -175,7 +175,7 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { reply.Update(title, body) } -func InviteMember(user std.Address, role Role) { // TODO: Implement suport for removing members +func InviteMember(user std.Address, role Role) { assertIsUserCall() caller := std.GetOrigCaller() @@ -189,6 +189,18 @@ func InviteMember(user std.Address, role Role) { // TODO: Implement suport for r }) } +func RemoveMember(user std.Address) { + assertIsUserCall() + + caller := std.GetOrigCaller() + gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(a Args) { + user := a[0].(std.Address) + if !gPerm.RemoveUser(user) { + panic("member not found") + } + }) +} + func assertIsUserCall() { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") From bb226e45fe78745f7e00aa9314d5acda5b05755d Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 15:02:02 +0100 Subject: [PATCH 15/18] chore: rename default permissions test file --- .../boards2/{permission_test.gno => permission_default_test.gno} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/gno.land/r/demo/boards2/{permission_test.gno => permission_default_test.gno} (100%) diff --git a/examples/gno.land/r/demo/boards2/permission_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/permission_test.gno rename to examples/gno.land/r/demo/boards2/permission_default_test.gno From ec0cab06f84b3ff68b3ca71b5872523bbbbf6f33 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 17:14:26 +0100 Subject: [PATCH 16/18] chore: fix filetest file names --- .../r/demo/boards2/{z_0a_filetest.gno => z_0_a_filetest.gno} | 0 .../r/demo/boards2/{z_0b_filetest.gno => z_0_b_filetest.gno} | 0 .../r/demo/boards2/{z_0c_filetest.gno => z_0_c_filetest.gno} | 0 .../r/demo/boards2/{z_0d_filetest.gno => z_0_d_filetest.gno} | 0 .../r/demo/boards2/{z_0e_filetest.gno => z_0_e_filetest.gno} | 0 .../r/demo/boards2/{z_0f_filetest.gno => z_0_f_filetest.gno} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename examples/gno.land/r/demo/boards2/{z_0a_filetest.gno => z_0_a_filetest.gno} (100%) rename examples/gno.land/r/demo/boards2/{z_0b_filetest.gno => z_0_b_filetest.gno} (100%) rename examples/gno.land/r/demo/boards2/{z_0c_filetest.gno => z_0_c_filetest.gno} (100%) rename examples/gno.land/r/demo/boards2/{z_0d_filetest.gno => z_0_d_filetest.gno} (100%) rename examples/gno.land/r/demo/boards2/{z_0e_filetest.gno => z_0_e_filetest.gno} (100%) rename examples/gno.land/r/demo/boards2/{z_0f_filetest.gno => z_0_f_filetest.gno} (100%) diff --git a/examples/gno.land/r/demo/boards2/z_0a_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0a_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_a_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/z_0b_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0b_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_b_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/z_0c_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0c_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_c_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/z_0d_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0d_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_d_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/z_0e_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0e_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_e_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/z_0f_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/z_0f_filetest.gno rename to examples/gno.land/r/demo/boards2/z_0_f_filetest.gno From 2986a17dc8e1c54632881f92dc09cc30b87adc8a Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 17:34:32 +0100 Subject: [PATCH 17/18] feat: add validations for user invite permission This validation makes sure only owners can invite other owners. --- .../gno.land/r/demo/boards2/permission.gno | 3 ++ .../r/demo/boards2/permission_default.gno | 9 +++--- .../r/demo/boards2/permission_handlers.gno | 16 ++++++++-- .../r/demo/boards2/z_1_a_filetest.gno | 24 +++++++++++++++ .../r/demo/boards2/z_1_b_filetest.gno | 29 ++++++++++++++++++ .../r/demo/boards2/z_1_c_filetest.gno | 30 +++++++++++++++++++ 6 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/z_1_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_1_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_1_c_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index e0cf44b814d..0fc5dc9515d 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -31,6 +31,9 @@ type ( // Permissioner define an interface to for permissioned execution. Permissioner interface { + // HasRole checks if a user has a specific role assigned. + HasRole(std.Address, Role) bool + // HasPermission checks if a user has a specific permission. HasPermission(std.Address, Permission) bool diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index ab4ea521f11..fc8efee14d2 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -12,7 +12,7 @@ type ( // PermissionsHandlerFunc defines a function to handle permission callbacks. // Handlers are called by the `WithPermission()` method to execute callbacks // when users have the permission assigned. - PermissionsHandlerFunc func(Args, *admindao.AdminDAO, func(Args)) + PermissionsHandlerFunc func(Permissioner, Args, func(Args)) // DefaultPermissions manages users, roles and permissions. DefaultPermissions struct { @@ -132,7 +132,7 @@ func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc // WithPermission calls a callback when a user has a specific permission. // It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. -func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, args Args, cb func(Args)) { +func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, args Args, cb func(Args)) { if !dp.HasPermission(user, perm) || !dp.dao.IsMember(user) { panic("unauthorized") } @@ -144,7 +144,7 @@ func (dp DefaultPermissions) WithPermission(user std.Address, perm Permission, a } fn := h.(PermissionsHandlerFunc) - fn(args, dp.dao, cb) + fn(dp, args, cb) } func createDefaultPermissions() *DefaultPermissions { @@ -156,13 +156,14 @@ func createDefaultPermissions() *DefaultPermissions { perms := NewDefaultPermissions( dao, WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionBoardCreate), + WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), // TODO: Finish assigning all roles and permissions // WithRole(RoleModerator, permissions...), WithUser(owner, RoleOwner), ) perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) + perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) return perms } diff --git a/examples/gno.land/r/demo/boards2/permission_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno index f012af4f8ac..9c6a24b90f0 100644 --- a/examples/gno.land/r/demo/boards2/permission_handlers.gno +++ b/examples/gno.land/r/demo/boards2/permission_handlers.gno @@ -3,11 +3,10 @@ package boards2 import ( "std" - "gno.land/p/demo/boards2/admindao" "gno.land/r/demo/users" ) -func handleBoardCreate(args Args, _ *admindao.AdminDAO, cb func(Args)) { +func handleBoardCreate(_ Permissioner, args Args, cb func(Args)) { // TODO: This way of dealing with arguments is delicate, ideally types should be used name := args[0].(string) if std.Address(name).IsValid() { @@ -24,3 +23,16 @@ func handleBoardCreate(args Args, _ *admindao.AdminDAO, cb func(Args)) { cb(args) } + +func handleMemberInvite(p Permissioner, args Args, cb func(Args)) { + // Make sure that only owners invite other owners + role := args[1].(Role) + if role == RoleOwner { + caller := std.GetOrigCaller() + if !p.HasRole(caller, RoleOwner) { + panic("only owners are allowed to invite other owners") + } + } + + cb(args) +} 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 new file mode 100644 index 00000000000..6497b4203e1 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.InviteMember(admin, boards2.RoleAdmin) + println("ok") +} + +// Output: +// 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 new file mode 100644 index 00000000000..eac31e48707 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_b_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 + user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") +) + +func init() { + // Add an admin user + std.TestSetOrigCaller(owner) + boards2.InviteMember(admin, boards2.RoleAdmin) + + // Next call will be done by the admin user + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.InviteMember(user, boards2.RoleOwner) +} + +// Error: +// only owners are allowed to invite other owners 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 new file mode 100644 index 00000000000..01ca55cb2f2 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") +) + +func init() { + // Add an admin user + std.TestSetOrigCaller(owner) + boards2.InviteMember(admin, boards2.RoleAdmin) + + // Next call will be done by the admin user + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.InviteMember(user, boards2.RoleAdmin) + println("ok") +} + +// Output: +// ok From db3f32b4252b312163eb1dd22251216119428a9c Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 27 Nov 2024 17:35:37 +0100 Subject: [PATCH 18/18] chore: correct variable name in some file tests --- examples/gno.land/r/demo/boards2/z_0_a_filetest.gno | 4 ++-- examples/gno.land/r/demo/boards2/z_0_b_filetest.gno | 4 ++-- examples/gno.land/r/demo/boards2/z_0_c_filetest.gno | 4 ++-- examples/gno.land/r/demo/boards2/z_0_e_filetest.gno | 4 ++-- examples/gno.land/r/demo/boards2/z_0_f_filetest.gno | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno index ea270767001..fc3781a78a8 100644 --- a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno @@ -6,10 +6,10 @@ import ( "gno.land/r/demo/boards2" ) -const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(owner) } func main() { diff --git a/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno index 1c24e10555f..08cbcf8ff43 100644 --- a/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno @@ -6,10 +6,10 @@ import ( "gno.land/r/demo/boards2" ) -const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(owner) } func main() { diff --git a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno index 6a7c13c99cc..e9a06b53e55 100644 --- a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno @@ -7,12 +7,12 @@ import ( ) const ( - admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 boardName = "test1" ) func init() { - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(owner) boards2.CreateBoard(boardName) } diff --git a/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno index c3032914822..33f45878b65 100644 --- a/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno @@ -6,10 +6,10 @@ import ( "gno.land/r/demo/boards2" ) -const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(owner) } func main() { diff --git a/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno index 1ff5ad840f4..f17f73bac4c 100644 --- a/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno @@ -6,10 +6,10 @@ import ( "gno.land/r/demo/boards2" ) -const admin = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(admin) + std.TestSetOrigCaller(owner) } func main() {