From 161f886774d1bae37379ce5ce72f9cd329144420 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:52:57 +0100 Subject: [PATCH 01/18] wip: teams v1 Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/action.gno | 143 ++++++++++++++++++ examples/gno.land/r/sys/teams/base.gno | 20 +++ examples/gno.land/r/sys/teams/eraseme.gno | 5 + examples/gno.land/r/sys/teams/gno.mod | 1 + examples/gno.land/r/sys/teams/teams.gno | 121 +++++++++++++++ .../r/sys/teams/teams_register_filetest.gno | 77 ++++++++++ examples/gno.land/r/sys/teams/teams_test.gno | 9 ++ .../gno.land/r/sys/teams/z_1_filetest.gno | 84 ++++++++++ 8 files changed, 460 insertions(+) create mode 100644 examples/gno.land/r/sys/teams/action.gno create mode 100644 examples/gno.land/r/sys/teams/base.gno create mode 100644 examples/gno.land/r/sys/teams/eraseme.gno create mode 100644 examples/gno.land/r/sys/teams/gno.mod create mode 100644 examples/gno.land/r/sys/teams/teams.gno create mode 100644 examples/gno.land/r/sys/teams/teams_register_filetest.gno create mode 100644 examples/gno.land/r/sys/teams/teams_test.gno create mode 100644 examples/gno.land/r/sys/teams/z_1_filetest.gno diff --git a/examples/gno.land/r/sys/teams/action.gno b/examples/gno.land/r/sys/teams/action.gno new file mode 100644 index 00000000000..c069fdb3ffc --- /dev/null +++ b/examples/gno.land/r/sys/teams/action.gno @@ -0,0 +1,143 @@ +package teams + +import "std" + +type ActionMsg interface{} + +type AddMemberMsg struct { + Member std.Address +} + +type RemoveMemberMsg struct { + Member std.Address +} + +type AddPackageMsg struct { + Path string +} + +// Core actions +const ( + AddMember ActionName = "add_member" + RemoveMember = "remove_member" + AddPackage = "add_pkg" + SetActionVisibility = "set_action_status" + AddAction = "add_action" // Allowed ? + UpdateAccessControl = "update_implem" // Allowed ? +) + +func (a ActionName) String() string { + return string(a) +} + +type ActionHandler func(t *Team, msg ActionMsg) + +type ActionDescription struct { + Action ActionName + Description string + Private bool + Deprecated bool + handler ActionHandler +} + +var actionDescriptions = []ActionDescription{ + { + Action: AddMember, + Description: "Adds a new member to the team.", + handler: handlerAddMember, + }, + { + Action: RemoveMember, + Description: "Removes an existing member from the team.", + handler: handlerRemoveMember, + }, + { + Action: AddPackage, + Description: "Adds a new package to the team's resources.", + handler: nil, // XXX + }, + { + Action: AddAction, + Description: "Registers a new action for the team.", + handler: handlerRegisterAction, + }, + { + Action: UpdateAccessControl, + Description: "Updates the access control interface of a team action.", + handler: nil, // XXX + }, +} + +type SetActionVisibilityMsg struct { + ActionName + Private bool +} + +func handlerSetActionVisibility(team *Team, msg ActionMsg) { + param, ok := msg.(SetActionVisibilityMsg) + if !ok { + panic("invalid arguments") + } + + action, ok := team.getAction(param.ActionName) + if !ok { + panic("action doesn't exsit") + } + + // Set new status + action.Private = param.Private + team.actions.Set(string(action.Action), action) +} + +func handlerRegisterAction(team *Team, msg ActionMsg) { + action, ok := msg.(ActionDescription) + if !ok { + panic("invalid arguments") + } + + if team.actions.Has(string(action.Action)) { + panic(ErrAleadyExist) + } + + team.actions.Set(string(action.Action), action) +} + +func handlerUpdateImplementation(team *Team, msg ActionMsg) { + action, ok := msg.(ActionDescription) + if ok { + panic("invalid arguments") + } + + if team.actions.Has(string(action.Action)) { + panic(ErrAleadyExist) + } + + team.actions.Set(string(action.Action), action) +} + +func handlerAddMember(team *Team, msg ActionMsg) { + member, ok := msg.(std.Address) + if !ok { + panic("invalid arguments") + } + + if team.members.Has(member.String()) { + panic(ErrAleadyExist) + } + team.members.Set(member.String(), struct{}{}) +} + +// func handlerAddPackage(team *Team, msg ActionMsg) {} + +func handlerRemoveMember(team *Team, msg ActionMsg) { + member, ok := msg.(std.Address) + if !ok { + panic("invalid arguments") + } + + if !team.members.Has(member.String()) { + panic(ErrDoesNotExist) + } + + team.members.Remove(member.String()) +} diff --git a/examples/gno.land/r/sys/teams/base.gno b/examples/gno.land/r/sys/teams/base.gno new file mode 100644 index 00000000000..c0c657d6695 --- /dev/null +++ b/examples/gno.land/r/sys/teams/base.gno @@ -0,0 +1,20 @@ +package teams + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type BaseTeam struct { + address std.Address + members avl.Tree // std.Address -> struct{}{} +} + +func (bt *BaseTeam) Has(member std.Address) bool { + return bt.members.Has(member.String()) +} + +func (bt *BaseTeam) Can(member std.Address, do ActionVerb, on ...std.Address) bool { + return true +} diff --git a/examples/gno.land/r/sys/teams/eraseme.gno b/examples/gno.land/r/sys/teams/eraseme.gno new file mode 100644 index 00000000000..af0c95d870b --- /dev/null +++ b/examples/gno.land/r/sys/teams/eraseme.gno @@ -0,0 +1,5 @@ +package teams + +type MyTeam struct { + BaseTeam +} diff --git a/examples/gno.land/r/sys/teams/gno.mod b/examples/gno.land/r/sys/teams/gno.mod new file mode 100644 index 00000000000..4afbd1a0fe9 --- /dev/null +++ b/examples/gno.land/r/sys/teams/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sys/teams \ No newline at end of file diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno new file mode 100644 index 00000000000..69c4da0fc48 --- /dev/null +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -0,0 +1,121 @@ +package teams + +import ( + "errors" + "regexp" + "std" + + "gno.land/p/demo/avl" +) + +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrAleadyExist = errors.New("already exist") + ErrDoesNotExist = errors.New("does not exist") + ErrInvalidArgument = errors.New("invalid argument") + ErrActionDoesNotExist = errors.New("action does not exist") +) + +var teams avl.Tree // std.Address -> Team + +type AccessController interface { + CanPerform(member std.Address, action ActionMsg) bool +} + +type Team struct { + std.Address + + ac AccessController + members avl.Tree // std.Address -> void + actions avl.Tree // append only Action -> ActionDescription +} + +func (team *Team) Perform(action ActionName, resource ActionMsg) { + actionDesc, ok := team.actions.Get(string(action)) + if !ok { + panic(ErrActionDoesNotExist) + } + + caller := std.GetOrigCaller() + if !team.ac.CanPerform(caller, action, resource) { + panic(ErrUnauthorized) + } + + handler(team, resource) +} + +func (team *Team) RegisterAction(action ActionDescription) { + team.Perform(AddAction, action) +} + +func (team *Team) OverrideAction(previous ActionName, new ActionDescription) { + previousAction, ok := team.getAction(previous) + if ok { + panic("previous action doesn't exist") + } + + if previousAction.Action == new.Action { + panic("cannot override a function with the same name") + } + + team.Perform(SetActionStatus, ActionStatusParam{ + Action: previous, + Status: Forbidden, + }) + team.Perform(AddAction, new) +} + +func (team *Team) getAction(action ActionName) (ad ActionDescription, ok bool) { + var val interface{} + if val, ok = team.actions.Get(string(action)); ok { + ad = val.(ActionDescription) + } + return ad, ok +} + +func Get(team std.Address) *Team { + if t, ok := teams.Get(team.String()); ok { + return t.(*Team) + } + return nil +} + +// realm of the form `xxx.xx/r//home` +var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) + +type unlimitedAC struct{} + +func (unlimitedAC) CanPerform(member std.Address, action ActionName, msg ActionMsg) bool { + return true +} + +func Register(ac AccessController, init func(*Team)) *Team { + caller := std.GetOrigCaller() + realm := std.PrevRealm() + + // check if origin caller is an home path + if !reHomeRealm.MatchString(realm.PkgPath()) { + panic("cannot register a team outside an home realm") + } + + // check if caller is not already registerer + if teams.Has(caller.String()) { + panic("team already registered: " + caller) + } + + // Init + team := &Team{ac: &unlimitedAC{}} + init(team) + + // Register user controller + team.ac = ac + + // Add the team + teams.Set(caller.String(), team) + + return team +} + +func IsRegister(team std.Address) bool { + return teams.Has(team.String()) +} diff --git a/examples/gno.land/r/sys/teams/teams_register_filetest.gno b/examples/gno.land/r/sys/teams/teams_register_filetest.gno new file mode 100644 index 00000000000..5f91f12eeaa --- /dev/null +++ b/examples/gno.land/r/sys/teams/teams_register_filetest.gno @@ -0,0 +1,77 @@ +// PKGPATH: gno.land/r/myteam/home +package home + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/r/sys/teams" +) + +type Level int + +const ( + LevelUnknown Level = iota // lowest + Level1 + Level2 + Level3 + Level4 +) + +const ( + Promote teams.Action = "promote" + Demote = "demote" +) + +var baseteam teams.BaseTeam +var teamAddress std.Address + +type Action teams.Action + +var team *Team + +type MyTeam struct { + levels avl.Tree // std.Address -> Level +} + +func (m *MyTeam) Can(member std.Address, do teams.Action, args ...string) bool { + var level Level + if mLevel, ok := m.levels.Get(member); ok { + level = mLevel + } + + // Base action + switch do { + case teams.Add: + if level >= Level3 { + return true + } + + case teams.Delete: + if level >= Level4 { + return true + } + + case teams.AddPkg: + if level >= Level1 { + return true + } + } + + return false // noop +} + +func (m *MyTeam) Promote(by std.Address, on ...std.Address) { + +} + +func init() { + teamAddress = std.GetOrigCaller() + + var myteam MyTeam + team = teams.Register(&myteam) +} + +func main() { + println(teams.IsRegister(teamAddress)) +} diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno new file mode 100644 index 00000000000..e88daf7db26 --- /dev/null +++ b/examples/gno.land/r/sys/teams/teams_test.gno @@ -0,0 +1,9 @@ +package teams + +import ( + "testing" +) + +func TestAVL(t *testing.T) { + t.Logf("hello") +} diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno new file mode 100644 index 00000000000..3993820cadd --- /dev/null +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -0,0 +1,84 @@ +// PKGPATH: gno.land/r/myteam/home +package home + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/r/sys/teams" +) + +type Level int + +const ( + LevelUnknown Level = iota // lowest + Level1 + Level2 + Level3 + Level4 +) + +const ( + Promote teams.Action = "promote" + Demote = "demote" +) + +var teamAddress std.Address + +var MyTeam team.Team + +type MyTeam struct { + *teams.Team + levels avl.Tree // std.Address -> Level +} + +func (m *MyTeam) CanPerform(member std.Address, action teams.Action, ressource interface{}) bool { + var level Level + if mLevel, ok := m.levels.Get(member); ok { + level = mLevel + } + + // Base action + switch do { + case teams.Add: + if level >= Level3 { + return true + } + + case teams.Delete: + if level >= Level4 { + return true + } + + case teams.AddPkg: + if level >= Level1 { + return true + } + } + + return false // noop +} + +type PromoteAction struct { + Level + Member std.Address +} + +func (m *MyTeam) Promote(member std.Address, toLevel Level) { + m.Perform(Promote, PomoteAction{ + Member: member, + Level: toLevel, + }) +} + +func init() { + teamAddress = std.GetOrigCaller() + myteam.Team = teams.Register(&myteam, func(t *Team) { + + t.RegisterAction + }) +} + +func main() { + println(teams.IsRegister(teamAddress)) +} From 0aa3665a0cc5d5e897c313f9bb05d4531534f0be Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:46:05 +0100 Subject: [PATCH 02/18] wip: teams v2 Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/action.gno | 168 +++++----------- examples/gno.land/r/sys/teams/base.gno | 20 -- examples/gno.land/r/sys/teams/eraseme.gno | 5 - examples/gno.land/r/sys/teams/msg.gno | 37 ++++ examples/gno.land/r/sys/teams/teams.gno | 186 ++++++++++++------ .../gno.land/r/sys/teams/teams_ownable.gno | 38 ++++ .../r/sys/teams/teams_register_filetest.gno | 77 -------- examples/gno.land/r/sys/teams/teams_test.gno | 8 - .../gno.land/r/sys/teams/z_1_filetest.gno | 164 +++++++++++---- 9 files changed, 376 insertions(+), 327 deletions(-) delete mode 100644 examples/gno.land/r/sys/teams/base.gno delete mode 100644 examples/gno.land/r/sys/teams/eraseme.gno create mode 100644 examples/gno.land/r/sys/teams/msg.gno create mode 100644 examples/gno.land/r/sys/teams/teams_ownable.gno delete mode 100644 examples/gno.land/r/sys/teams/teams_register_filetest.gno diff --git a/examples/gno.land/r/sys/teams/action.gno b/examples/gno.land/r/sys/teams/action.gno index c069fdb3ffc..f0dccffec93 100644 --- a/examples/gno.land/r/sys/teams/action.gno +++ b/examples/gno.land/r/sys/teams/action.gno @@ -1,143 +1,69 @@ package teams -import "std" +type ActionFunc func(t *Team) Msg -type ActionMsg interface{} - -type AddMemberMsg struct { - Member std.Address -} - -type RemoveMemberMsg struct { - Member std.Address -} - -type AddPackageMsg struct { - Path string -} - -// Core actions -const ( - AddMember ActionName = "add_member" - RemoveMember = "remove_member" - AddPackage = "add_pkg" - SetActionVisibility = "set_action_status" - AddAction = "add_action" // Allowed ? - UpdateAccessControl = "update_implem" // Allowed ? -) - -func (a ActionName) String() string { - return string(a) -} - -type ActionHandler func(t *Team, msg ActionMsg) - -type ActionDescription struct { - Action ActionName - Description string - Private bool - Deprecated bool - handler ActionHandler +type Action interface { + call(t *Team) Msg } -var actionDescriptions = []ActionDescription{ - { - Action: AddMember, - Description: "Adds a new member to the team.", - handler: handlerAddMember, - }, - { - Action: RemoveMember, - Description: "Removes an existing member from the team.", - handler: handlerRemoveMember, - }, - { - Action: AddPackage, - Description: "Adds a new package to the team's resources.", - handler: nil, // XXX - }, - { - Action: AddAction, - Description: "Registers a new action for the team.", - handler: handlerRegisterAction, - }, - { - Action: UpdateAccessControl, - Description: "Updates the access control interface of a team action.", - handler: nil, // XXX - }, +type action struct { + actionFunc ActionFunc } -type SetActionVisibilityMsg struct { - ActionName - Private bool +func (a action) call(t *Team) Msg { + return a.actionFunc(t) } -func handlerSetActionVisibility(team *Team, msg ActionMsg) { - param, ok := msg.(SetActionVisibilityMsg) - if !ok { - panic("invalid arguments") +func ActionableMsg(msg ...Msg) Action { + switch len(msg) { + case 0: + return nil + case 1: + return Actionable(func(_ *Team) Msg { + return msg[0] + }) + default: } - action, ok := team.getAction(param.ActionName) - if !ok { - panic("action doesn't exsit") + fns := make([]ActionFunc, len(msg)) + for i, m := range msg { + fns[i] = func(_ *Team) Msg { + return m + } } - - // Set new status - action.Private = param.Private - team.actions.Set(string(action.Action), action) + return Actionable(fns...) } -func handlerRegisterAction(team *Team, msg ActionMsg) { - action, ok := msg.(ActionDescription) - if !ok { - panic("invalid arguments") +func Actionable(fn ...ActionFunc) Action { + switch len(fn) { + case 0: + return nil + case 1: + return &action{actionFunc: fn[0]} + default: } - if team.actions.Has(string(action.Action)) { - panic(ErrAleadyExist) + actions := make([]Action, len(fn)) + for i, f := range fn { + actions[i] = &action{actionFunc: f} } - - team.actions.Set(string(action.Action), action) + return ChainActions(actions...) } -func handlerUpdateImplementation(team *Team, msg ActionMsg) { - action, ok := msg.(ActionDescription) - if ok { - panic("invalid arguments") - } - - if team.actions.Has(string(action.Action)) { - panic(ErrAleadyExist) - } - - team.actions.Set(string(action.Action), action) -} - -func handlerAddMember(team *Team, msg ActionMsg) { - member, ok := msg.(std.Address) - if !ok { - panic("invalid arguments") - } - - if team.members.Has(member.String()) { - panic(ErrAleadyExist) - } - team.members.Set(member.String(), struct{}{}) -} - -// func handlerAddPackage(team *Team, msg ActionMsg) {} - -func handlerRemoveMember(team *Team, msg ActionMsg) { - member, ok := msg.(std.Address) - if !ok { - panic("invalid arguments") - } - - if !team.members.Has(member.String()) { - panic(ErrDoesNotExist) +func ChainActions(actions ...Action) Action { + switch len(actions) { + case 0: + return nil + case 1: + return actions[0] + default: } - team.members.Remove(member.String()) + return Actionable(func(t *Team) Msg { + msgs := make([]Msg, len(actions)) + for i, action := range actions { + msgs[i] = action.call(t) + } + return msgs + }) } diff --git a/examples/gno.land/r/sys/teams/base.gno b/examples/gno.land/r/sys/teams/base.gno deleted file mode 100644 index c0c657d6695..00000000000 --- a/examples/gno.land/r/sys/teams/base.gno +++ /dev/null @@ -1,20 +0,0 @@ -package teams - -import ( - "std" - - "gno.land/p/demo/avl" -) - -type BaseTeam struct { - address std.Address - members avl.Tree // std.Address -> struct{}{} -} - -func (bt *BaseTeam) Has(member std.Address) bool { - return bt.members.Has(member.String()) -} - -func (bt *BaseTeam) Can(member std.Address, do ActionVerb, on ...std.Address) bool { - return true -} diff --git a/examples/gno.land/r/sys/teams/eraseme.gno b/examples/gno.land/r/sys/teams/eraseme.gno deleted file mode 100644 index af0c95d870b..00000000000 --- a/examples/gno.land/r/sys/teams/eraseme.gno +++ /dev/null @@ -1,5 +0,0 @@ -package teams - -type MyTeam struct { - BaseTeam -} diff --git a/examples/gno.land/r/sys/teams/msg.gno b/examples/gno.land/r/sys/teams/msg.gno new file mode 100644 index 00000000000..5d1fdc9fc6e --- /dev/null +++ b/examples/gno.land/r/sys/teams/msg.gno @@ -0,0 +1,37 @@ +package teams + +import "std" + +type Msg interface{} + +type AddMemberMsg struct { + Member std.Address +} + +func AddMemberAction(member std.Address) Action { + return Actionable(func(t *Team) Msg { + if t.members.Has(member.String()) { + panic(ErrAleadyExist) + } + t.members.Set(member.String(), struct{}{}) + return nil + }) +} + +type RemoveMemberMsg struct { + Member std.Address +} + +func RemoveMemberAction(member std.Address) Action { + return Actionable(func(t *Team) Msg { + if t.members.Has(member.String()) { + panic(ErrAleadyExist) + } + t.members.Set(member.String(), struct{}{}) + return nil + }) +} + +type AddPackageMsg struct { + Path string +} diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index 69c4da0fc48..72d28030b82 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -9,113 +9,187 @@ import ( ) var ( - ErrUnauthorized = errors.New("unauthorized") - ErrAleadyExist = errors.New("already exist") - ErrDoesNotExist = errors.New("does not exist") - ErrInvalidArgument = errors.New("invalid argument") - ErrActionDoesNotExist = errors.New("action does not exist") + ErrUnauthorized = errors.New("unauthorized") + ErrAleadyExist = errors.New("already exist") + ErrDoesNotExist = errors.New("does not exist") ) -var teams avl.Tree // std.Address -> Team - type AccessController interface { - CanPerform(member std.Address, action ActionMsg) bool + CanPerform(member std.Address, msg Msg) bool +} + +type Lifecycle interface { + Init() Action + Update(team *Team, msg Msg) Action +} + +type ITeam interface { + AccessController + Lifecycle } +var teams avl.Tree // std.Address -> Team + type Team struct { - std.Address + AccessController + Lifecycle - ac AccessController + address std.Address members avl.Tree // std.Address -> void - actions avl.Tree // append only Action -> ActionDescription } -func (team *Team) Perform(action ActionName, resource ActionMsg) { - actionDesc, ok := team.actions.Get(string(action)) - if !ok { - panic(ErrActionDoesNotExist) - } - +func (team *Team) Perform(msgs ...Msg) { caller := std.GetOrigCaller() - if !team.ac.CanPerform(caller, action, resource) { - panic(ErrUnauthorized) + if !team.IsMember(caller) { + panic("only member can perform action on team") } - handler(team, resource) + // get actions for the given msgs + actions := team.getActionsForMsg(msgs...) + team.performActions(actions...) } -func (team *Team) RegisterAction(action ActionDescription) { - team.Perform(AddAction, action) +func (team *Team) AddMember(member std.Address) { + team.Perform(AddMemberMsg{ + Member: member, + }) } -func (team *Team) OverrideAction(previous ActionName, new ActionDescription) { - previousAction, ok := team.getAction(previous) - if ok { - panic("previous action doesn't exist") +func (team *Team) RemoveMember(member std.Address) { + team.Perform(RemoveMemberMsg{ + Member: member, + }) +} + +func (team *Team) HasMember(member std.Address) bool { + return team.members.Has(member.String()) +} + +type assertPerformDefaultMsg struct{ assert bool } + +func (team *Team) assertPerformDefault() { + action := team.Lifecycle.Update(team, assertPerformDefaultMsg{}) + msg := action.call(team) + if assertMsg, ok := msg.(assertPerformDefaultMsg); ok { + if assertMsg.assert { + return + } } - if previousAction.Action == new.Action { - panic("cannot override a function with the same name") + panic(`make sure that team implementation handle team update fallback`) +} + +func (team *Team) PerformDefault(msg Msg) Action { + switch typ := msg.(type) { + case AddMemberMsg: + return AddMemberAction(typ.Member) + case RemoveMemberMsg: + return RemoveMemberAction(typ.Member) + case AddPackageMsg: // Do nothing + + case assertPerformDefaultMsg: + typ.assert = true + return ActionableMsg(typ) } - team.Perform(SetActionStatus, ActionStatusParam{ - Action: previous, - Status: Forbidden, - }) - team.Perform(AddAction, new) + return nil } -func (team *Team) getAction(action ActionName) (ad ActionDescription, ok bool) { - var val interface{} - if val, ok = team.actions.Get(string(action)); ok { - ad = val.(ActionDescription) +func (team *Team) performActions(actions ...Action) { + var action Action + for len(actions) > 0 { + action, actions = actions[0], actions[1:] // shift action + if action == nil { + + continue // skip empty action + } + + if msg := action.call(team); msg != nil { + nextActions := team.getActionsForMsg(msg) + actions = append(nextActions, actions...) + } } - return ad, ok } -func Get(team std.Address) *Team { - if t, ok := teams.Get(team.String()); ok { - return t.(*Team) +func (team *Team) getActionsForMsg(msgs ...Msg) []Action { + caller := std.GetOrigCaller() + actions := make([]Action, 0, len(msgs)) + for _, msg := range msgs { + if msg == nil { + continue // skip empty msg + } + + switch typ := msg.(type) { + case []Msg: + subActions := team.getActionsForMsg(typ...) + actions = append(actions, subActions...) + default: + // Assert caller can perform action + if !team.IsTeamAddress(caller) && !team.AccessController.CanPerform(caller, msg) { + panic("unauthorized: " + caller.String()) + } + + // Prepare action + actions = append(actions, team.Lifecycle.Update(team, msg)) + } } - return nil + return actions } -// realm of the form `xxx.xx/r//home` -var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) +func (team *Team) IsTeamAddress(teamAddr std.Address) bool { + return teamAddr == team.address +} + +func (team *Team) IsMember(member std.Address) bool { + return member == team.address || team.members.Has(member.String()) +} type unlimitedAC struct{} -func (unlimitedAC) CanPerform(member std.Address, action ActionName, msg ActionMsg) bool { +func (unlimitedAC) CanPerform(member std.Address, msg Msg) bool { return true } -func Register(ac AccessController, init func(*Team)) *Team { +// realm of the form `xxx.xx/r//home` +var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) + +func Register(iteam ITeam) *Team { caller := std.GetOrigCaller() + println(caller) realm := std.PrevRealm() + // First lets check the realm is valid + // check if origin caller is an home path if !reHomeRealm.MatchString(realm.PkgPath()) { panic("cannot register a team outside an home realm") } - // check if caller is not already registerer + // check if caller is not already registerer as a team if teams.Has(caller.String()) { panic("team already registered: " + caller) } - // Init - team := &Team{ac: &unlimitedAC{}} - init(team) + // Then initilize the team + team := &Team{address: caller, Lifecycle: iteam} - // Register user controller - team.ac = ac + // Assert that team implementation correctly use fallback + // XXX: do we want this ? + // > it assert that caller have a minimal implementation of his team + team.assertPerformDefault() - // Add the team - teams.Set(caller.String(), team) + if initAction := iteam.Init(); initAction != nil { + // init is performed using an unlimited ac + team.AccessController = unlimitedAC{} + team.performActions(initAction) + } + // once done, apply the provided implementation ac + team.AccessController = iteam + teams.Set(caller.String(), team) return team } -func IsRegister(team std.Address) bool { - return teams.Has(team.String()) +func IsRegister(teamAddr std.Address) bool { + return teams.Has(teamAddr.String()) } diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno new file mode 100644 index 00000000000..58019988bdc --- /dev/null +++ b/examples/gno.land/r/sys/teams/teams_ownable.gno @@ -0,0 +1,38 @@ +package teams + +import ( + "std" + + "gno.land/p/demo/ownable" +) + +type OwnableAccessController struct { + *ownable.Ownable + + EnableAddMember bool + EnableRemoveMember bool + DisableAddPackage bool +} + +func NewOwnableTeam(ownable *ownable.Ownable) *OwnableAccessController { + return &OwnableAccessController{Ownable: ownable} +} + +func (o *OwnableAccessController) CanPerform(member std.Address, msg Msg) bool { + switch msg.(type) { + case AddMemberMsg: + if o.EnableAddMember { + return true + } + case RemoveMemberMsg: + if o.EnableRemoveMember { + return true + } + case AddPackageMsg: + if o.DisableAddPackage { + return false + } + } + + return o.Ownable.Owner() == member +} diff --git a/examples/gno.land/r/sys/teams/teams_register_filetest.gno b/examples/gno.land/r/sys/teams/teams_register_filetest.gno deleted file mode 100644 index 5f91f12eeaa..00000000000 --- a/examples/gno.land/r/sys/teams/teams_register_filetest.gno +++ /dev/null @@ -1,77 +0,0 @@ -// PKGPATH: gno.land/r/myteam/home -package home - -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/r/sys/teams" -) - -type Level int - -const ( - LevelUnknown Level = iota // lowest - Level1 - Level2 - Level3 - Level4 -) - -const ( - Promote teams.Action = "promote" - Demote = "demote" -) - -var baseteam teams.BaseTeam -var teamAddress std.Address - -type Action teams.Action - -var team *Team - -type MyTeam struct { - levels avl.Tree // std.Address -> Level -} - -func (m *MyTeam) Can(member std.Address, do teams.Action, args ...string) bool { - var level Level - if mLevel, ok := m.levels.Get(member); ok { - level = mLevel - } - - // Base action - switch do { - case teams.Add: - if level >= Level3 { - return true - } - - case teams.Delete: - if level >= Level4 { - return true - } - - case teams.AddPkg: - if level >= Level1 { - return true - } - } - - return false // noop -} - -func (m *MyTeam) Promote(by std.Address, on ...std.Address) { - -} - -func init() { - teamAddress = std.GetOrigCaller() - - var myteam MyTeam - team = teams.Register(&myteam) -} - -func main() { - println(teams.IsRegister(teamAddress)) -} diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno index e88daf7db26..bd827e5e875 100644 --- a/examples/gno.land/r/sys/teams/teams_test.gno +++ b/examples/gno.land/r/sys/teams/teams_test.gno @@ -1,9 +1 @@ package teams - -import ( - "testing" -) - -func TestAVL(t *testing.T) { - t.Logf("hello") -} diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index 3993820cadd..a8267bad107 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -5,9 +5,12 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" "gno.land/r/sys/teams" ) +// var myteam *Team + type Level int const ( @@ -18,67 +21,148 @@ const ( Level4 ) -const ( - Promote teams.Action = "promote" - Demote = "demote" -) - -var teamAddress std.Address - -var MyTeam team.Team - type MyTeam struct { *teams.Team - levels avl.Tree // std.Address -> Level + address std.Address + levels avl.Tree // std.Address -> Level +} + +func (m *MyTeam) Init() teams.Action { + caller := std.GetOrigCaller() + return teams.ActionableMsg( + // Add caller as member + teams.AddMemberMsg{caller}, + // Promote caller to level4 + SetLevelMsg{caller, Level4}, + ) } -func (m *MyTeam) CanPerform(member std.Address, action teams.Action, ressource interface{}) bool { +func (m *MyTeam) CanPerform(member std.Address, msg teams.Msg) bool { var level Level - if mLevel, ok := m.levels.Get(member); ok { - level = mLevel + if mLevel, ok := m.levels.Get(member.String()); ok { + level = mLevel.(Level) + } + + shouldLevelMinimum := func(target Level) bool { + return level >= target } // Base action - switch do { - case teams.Add: - if level >= Level3 { - return true - } - - case teams.Delete: - if level >= Level4 { - return true - } - - case teams.AddPkg: - if level >= Level1 { - return true - } + switch msg.(type) { + case SetLevelMsg: + return shouldLevelMinimum(Level4) + case teams.RemoveMemberMsg: + return shouldLevelMinimum(Level3) + case teams.AddMemberMsg: + return shouldLevelMinimum(Level2) + case teams.AddPackageMsg: + return shouldLevelMinimum(Level1) } - return false // noop + return false } -type PromoteAction struct { - Level +func (m *MyTeam) Update(team *teams.Team, msg teams.Msg) teams.Action { + switch typ := msg.(type) { + case SetLevelMsg: + return teams.Actionable(func(_ *teams.Team) teams.Msg { + mkey := typ.Member.String() + m.levels.Set(mkey, typ.Level) + return nil + }) + case teams.AddMemberMsg: + return teams.ChainActions( + // Add a new member + teams.AddMemberAction(typ.Member), + // Promote it to level 1 + teams.ActionableMsg(SetLevelMsg{ + Member: typ.Member, + Level: Level1, + }), + ) + } + + return team.PerformDefault(msg) +} + +type SetLevelMsg struct { Member std.Address + Level } -func (m *MyTeam) Promote(member std.Address, toLevel Level) { - m.Perform(Promote, PomoteAction{ +func (m *MyTeam) GetLevel(member std.Address) Level { + if level, ok := m.levels.Get(member.String()); ok { + return level.(Level) + } + return LevelUnknown +} + +func (m *MyTeam) SetLevel(member std.Address, level Level) { + m.Team.Perform(SetLevelMsg{ Member: member, - Level: toLevel, + Level: level, }) } -func init() { - teamAddress = std.GetOrigCaller() - myteam.Team = teams.Register(&myteam, func(t *Team) { +var myteam MyTeam - t.RegisterAction - }) +func init() { + myteam.Team = teams.Register(&myteam) } func main() { - println(teams.IsRegister(teamAddress)) + // Setup user for test + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + + println("myteam can add a package:", myteam.CanPerform(alice, teams.AddPackageMsg{})) + + // Register alice to the team + myteam.AddMember(alice) + println("alice is member: ", myteam.HasMember(alice)) + println("alice is level 1: ", myteam.GetLevel(alice) == Level1) + + // Should be able to add a package on level1 + println("alice can add package:", myteam.CanPerform(alice, teams.AddPackageMsg{})) + // Should be able to add a package on level1 + println("bob cannot add package:", myteam.CanPerform(bob, teams.AddPackageMsg{})) + + // Should not be able to add a member on level1 + println("alice cannot add bob as member:", + myteam.CanPerform(alice, teams.AddMemberMsg{ + Member: bob, + })) + + // Update alice to Level4 + myteam.SetLevel(alice, Level4) + println("alice is level 4: ", myteam.GetLevel(alice) == Level4) + println("alice can add bob as member:", + myteam.CanPerform(alice, teams.AddMemberMsg{ + Member: bob, + })) + + // Set caller to alice + std.TestSetOrigCaller(alice) + + // alice add member bob + println("adding bob") + myteam.AddMember(bob) + println("bob is member: ", myteam.HasMember(alice)) + println("bob is level 1: ", myteam.GetLevel(alice) == Level1) + + // Check if alice can perform add package + // println(teams.CanPerform(myTeamUser, alice, teams.AddPackageMsg{})) + + // Set alice as caller + // std.TestSetOrigCaller(alice) + // Try to perform add package + + // println(teams.CanPerform(myTeamUser, alice, teams.AddPackageMsg{})) + + // bob := testutils.TestAddress("bob") + + // println(teams.IsRegister(myteam.address)) + // myteam.address = teams.Register(&myteam) + // println(teams.IsRegister(myteam.address)) + } From 440554235b6e7be5faed6161385ef343fc73ff2a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 00:58:30 +0100 Subject: [PATCH 03/18] wip: teams implem Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/action.gno | 69 ------- examples/gno.land/r/sys/teams/cmd.gno | 42 +++++ examples/gno.land/r/sys/teams/msg.gno | 37 ---- examples/gno.land/r/sys/teams/task.gno | 69 +++++++ examples/gno.land/r/sys/teams/teams.gno | 168 +++++++++--------- .../gno.land/r/sys/teams/teams_ownable.gno | 12 +- .../gno.land/r/sys/teams/z_1_filetest.gno | 94 ++++------ gnovm/pkg/gnolang/values.go | 2 +- 8 files changed, 242 insertions(+), 251 deletions(-) delete mode 100644 examples/gno.land/r/sys/teams/action.gno create mode 100644 examples/gno.land/r/sys/teams/cmd.gno delete mode 100644 examples/gno.land/r/sys/teams/msg.gno create mode 100644 examples/gno.land/r/sys/teams/task.gno diff --git a/examples/gno.land/r/sys/teams/action.gno b/examples/gno.land/r/sys/teams/action.gno deleted file mode 100644 index f0dccffec93..00000000000 --- a/examples/gno.land/r/sys/teams/action.gno +++ /dev/null @@ -1,69 +0,0 @@ -package teams - -type ActionFunc func(t *Team) Msg - -type Action interface { - call(t *Team) Msg -} - -type action struct { - actionFunc ActionFunc -} - -func (a action) call(t *Team) Msg { - return a.actionFunc(t) -} - -func ActionableMsg(msg ...Msg) Action { - switch len(msg) { - case 0: - return nil - case 1: - return Actionable(func(_ *Team) Msg { - return msg[0] - }) - default: - } - - fns := make([]ActionFunc, len(msg)) - for i, m := range msg { - fns[i] = func(_ *Team) Msg { - return m - } - } - return Actionable(fns...) -} - -func Actionable(fn ...ActionFunc) Action { - switch len(fn) { - case 0: - return nil - case 1: - return &action{actionFunc: fn[0]} - default: - } - - actions := make([]Action, len(fn)) - for i, f := range fn { - actions[i] = &action{actionFunc: f} - } - return ChainActions(actions...) -} - -func ChainActions(actions ...Action) Action { - switch len(actions) { - case 0: - return nil - case 1: - return actions[0] - default: - } - - return Actionable(func(t *Team) Msg { - msgs := make([]Msg, len(actions)) - for i, action := range actions { - msgs[i] = action.call(t) - } - return msgs - }) -} diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno new file mode 100644 index 00000000000..a997a1933c0 --- /dev/null +++ b/examples/gno.land/r/sys/teams/cmd.gno @@ -0,0 +1,42 @@ +package teams + +import ( + "errors" + "std" +) + +var ErrAlreadyExist = errors.New("already exist") + +type Cmd interface{} + +type AddMemberCmd struct { + Member std.Address +} + +func AddMemberTask(member std.Address) Task { + return CreateTask(func(t *Team) Cmd { + if t.members.Has(member.String()) { + panic(ErrAlreadyExist) + } + t.members.Set(member.String(), struct{}{}) + return nil + }) +} + +type RemoveMemberCmd struct { + Member std.Address +} + +func RemoveMemberTask(member std.Address) Task { + return CreateTask(func(t *Team) Cmd { + if t.members.Has(member.String()) { + panic(ErrAlreadyExist) + } + t.members.Set(member.String(), struct{}{}) + return nil + }) +} + +type AddPackageCmd struct { + Path string +} diff --git a/examples/gno.land/r/sys/teams/msg.gno b/examples/gno.land/r/sys/teams/msg.gno deleted file mode 100644 index 5d1fdc9fc6e..00000000000 --- a/examples/gno.land/r/sys/teams/msg.gno +++ /dev/null @@ -1,37 +0,0 @@ -package teams - -import "std" - -type Msg interface{} - -type AddMemberMsg struct { - Member std.Address -} - -func AddMemberAction(member std.Address) Action { - return Actionable(func(t *Team) Msg { - if t.members.Has(member.String()) { - panic(ErrAleadyExist) - } - t.members.Set(member.String(), struct{}{}) - return nil - }) -} - -type RemoveMemberMsg struct { - Member std.Address -} - -func RemoveMemberAction(member std.Address) Action { - return Actionable(func(t *Team) Msg { - if t.members.Has(member.String()) { - panic(ErrAleadyExist) - } - t.members.Set(member.String(), struct{}{}) - return nil - }) -} - -type AddPackageMsg struct { - Path string -} diff --git a/examples/gno.land/r/sys/teams/task.gno b/examples/gno.land/r/sys/teams/task.gno new file mode 100644 index 00000000000..f6f3561ee2e --- /dev/null +++ b/examples/gno.land/r/sys/teams/task.gno @@ -0,0 +1,69 @@ +package teams + +type TaskFunc func(t *Team) Cmd + +type Task interface { + call(t *Team) Cmd +} + +type task struct { + actionFunc TaskFunc +} + +func (a task) call(t *Team) Cmd { + return a.actionFunc(t) +} + +func CreateTaskCmd(cmd ...Cmd) Task { + switch len(cmd) { + case 0: + return nil + case 1: + return CreateTask(func(_ *Team) Cmd { + return cmd[0] + }) + default: + } + + fns := make([]TaskFunc, len(cmd)) + for i, m := range cmd { + fns[i] = func(_ *Team) Cmd { + return m + } + } + return CreateTask(fns...) +} + +func CreateTask(fn ...TaskFunc) Task { + switch len(fn) { + case 0: + return nil + case 1: + return &task{actionFunc: fn[0]} + default: + } + + actions := make([]Task, len(fn)) + for i, f := range fn { + actions[i] = &task{actionFunc: f} + } + return ChainTasks(actions...) +} + +func ChainTasks(actions ...Task) Task { + switch len(actions) { + case 0: + return nil + case 1: + return actions[0] + default: + } + + return CreateTask(func(t *Team) Cmd { + cmds := make([]Cmd, len(actions)) + for i, action := range actions { + cmds[i] = action.call(t) + } + return cmds + }) +} diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index 72d28030b82..9039e9cc326 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -1,26 +1,19 @@ package teams import ( - "errors" "regexp" "std" "gno.land/p/demo/avl" ) -var ( - ErrUnauthorized = errors.New("unauthorized") - ErrAleadyExist = errors.New("already exist") - ErrDoesNotExist = errors.New("does not exist") -) - type AccessController interface { - CanPerform(member std.Address, msg Msg) bool + CanRun(member std.Address, cmd Cmd) bool } type Lifecycle interface { - Init() Action - Update(team *Team, msg Msg) Action + Init() Task + ApplyUpdate(team *Team, cmd Cmd) Task } type ITeam interface { @@ -38,115 +31,112 @@ type Team struct { members avl.Tree // std.Address -> void } -func (team *Team) Perform(msgs ...Msg) { +func (team *Team) Run(cmds ...Cmd) { caller := std.GetOrigCaller() if !team.IsMember(caller) { - panic("only member can perform action on team") + panic("only member can perform command on team") } - // get actions for the given msgs - actions := team.getActionsForMsg(msgs...) - team.performActions(actions...) + // get actions for the given cmds + actions := team.getTasksForCmds(cmds...) + team.performTasks(actions...) +} + +func (team *Team) CanAddPackage(member std.Address, path string) bool { + return team.CanRun(member, AddPackageCmd{Path: path}) } func (team *Team) AddMember(member std.Address) { - team.Perform(AddMemberMsg{ - Member: member, - }) + team.Run(AddMemberCmd{Member: member}) +} + +func (team *Team) CanAddMember(member, target std.Address) bool { + return team.CanRun(member, AddMemberCmd{Member: member}) } func (team *Team) RemoveMember(member std.Address) { - team.Perform(RemoveMemberMsg{ - Member: member, - }) + team.Run(RemoveMemberCmd{Member: member}) +} + +func (team *Team) CanRemoveMember(member, target std.Address) bool { + return team.CanRun(member, RemoveMemberCmd{Member: target}) } func (team *Team) HasMember(member std.Address) bool { return team.members.Has(member.String()) } -type assertPerformDefaultMsg struct{ assert bool } +func (team *Team) CanRun(caller std.Address, cmd Cmd) bool { + return !team.IsTeamAddress(caller) && !team.AccessController.CanRun(caller, cmd) +} -func (team *Team) assertPerformDefault() { - action := team.Lifecycle.Update(team, assertPerformDefaultMsg{}) - msg := action.call(team) - if assertMsg, ok := msg.(assertPerformDefaultMsg); ok { - if assertMsg.assert { - return - } - } +func (team *Team) IsTeamAddress(teamAddr std.Address) bool { + return teamAddr == team.address +} - panic(`make sure that team implementation handle team update fallback`) +func (team *Team) IsMember(member std.Address) bool { + return member == team.address || team.members.Has(member.String()) } -func (team *Team) PerformDefault(msg Msg) Action { - switch typ := msg.(type) { - case AddMemberMsg: - return AddMemberAction(typ.Member) - case RemoveMemberMsg: - return RemoveMemberAction(typ.Member) - case AddPackageMsg: // Do nothing +func (team *Team) ApplyDefault(cmd Cmd) Task { + switch typ := cmd.(type) { + case AddMemberCmd: + return AddMemberTask(typ.Member) + case RemoveMemberCmd: + return RemoveMemberTask(typ.Member) + case AddPackageCmd: // Do nothing - case assertPerformDefaultMsg: + case assertRunDefaultCmd: typ.assert = true - return ActionableMsg(typ) + return CreateTaskCmd(typ) } return nil } -func (team *Team) performActions(actions ...Action) { - var action Action - for len(actions) > 0 { - action, actions = actions[0], actions[1:] // shift action - if action == nil { - - continue // skip empty action +func (team *Team) performTasks(tasks ...Task) { + var task Task + for len(tasks) > 0 { + task, tasks = tasks[0], tasks[1:] // shift task + if task == nil { + continue // skip empty task } - if msg := action.call(team); msg != nil { - nextActions := team.getActionsForMsg(msg) - actions = append(nextActions, actions...) + if cmd := task.call(team); cmd != nil { + nextTasks := team.getTasksForCmds(cmd) + tasks = append(nextTasks, tasks...) } } } -func (team *Team) getActionsForMsg(msgs ...Msg) []Action { +func (team *Team) getTasksForCmds(cmds ...Cmd) []Task { caller := std.GetOrigCaller() - actions := make([]Action, 0, len(msgs)) - for _, msg := range msgs { - if msg == nil { - continue // skip empty msg + tasks := make([]Task, 0, len(cmds)) + for _, cmd := range cmds { + if cmd == nil { + continue // skip empty cmd } - switch typ := msg.(type) { - case []Msg: - subActions := team.getActionsForMsg(typ...) - actions = append(actions, subActions...) + switch typ := cmd.(type) { + case []Cmd: + subTasks := team.getTasksForCmds(typ...) + tasks = append(tasks, subTasks...) default: - // Assert caller can perform action - if !team.IsTeamAddress(caller) && !team.AccessController.CanPerform(caller, msg) { - panic("unauthorized: " + caller.String()) + // Assert caller can perform task + if team.CanRun(caller, cmd) { + panic("unauthorized") } - // Prepare action - actions = append(actions, team.Lifecycle.Update(team, msg)) + // Prepare task + tasks = append(tasks, team.Lifecycle.ApplyUpdate(team, cmd)) } } - return actions -} - -func (team *Team) IsTeamAddress(teamAddr std.Address) bool { - return teamAddr == team.address -} - -func (team *Team) IsMember(member std.Address) bool { - return member == team.address || team.members.Has(member.String()) + return tasks } type unlimitedAC struct{} -func (unlimitedAC) CanPerform(member std.Address, msg Msg) bool { +func (unlimitedAC) CanRun(member std.Address, cmd Cmd) bool { return true } @@ -155,17 +145,19 @@ var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z] func Register(iteam ITeam) *Team { caller := std.GetOrigCaller() - println(caller) realm := std.PrevRealm() // First lets check the realm is valid + if iteam == nil { + panic("team cannot be nil") + } - // check if origin caller is an home path + // Check if origin caller is an home path if !reHomeRealm.MatchString(realm.PkgPath()) { panic("cannot register a team outside an home realm") } - // check if caller is not already registerer as a team + // Check if caller is not already registerer as a team if teams.Has(caller.String()) { panic("team already registered: " + caller) } @@ -176,15 +168,15 @@ func Register(iteam ITeam) *Team { // Assert that team implementation correctly use fallback // XXX: do we want this ? // > it assert that caller have a minimal implementation of his team - team.assertPerformDefault() + team.assertRunDefault() - if initAction := iteam.Init(); initAction != nil { + if initTask := iteam.Init(); initTask != nil { // init is performed using an unlimited ac team.AccessController = unlimitedAC{} - team.performActions(initAction) + team.performTasks(initTask) } - // once done, apply the provided implementation ac + // Once done, apply the provided implementation ac team.AccessController = iteam teams.Set(caller.String(), team) return team @@ -193,3 +185,17 @@ func Register(iteam ITeam) *Team { func IsRegister(teamAddr std.Address) bool { return teams.Has(teamAddr.String()) } + +type assertRunDefaultCmd struct{ assert bool } + +func (team *Team) assertRunDefault() { + task := team.Lifecycle.ApplyUpdate(team, assertRunDefaultCmd{}) + cmd := task.call(team) + if assertCmd, ok := cmd.(assertRunDefaultCmd); ok { + if assertCmd.assert { + return + } + } + + panic(`make sure that team implementation handle team update fallback`) +} diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno index 58019988bdc..c0e6bda089a 100644 --- a/examples/gno.land/r/sys/teams/teams_ownable.gno +++ b/examples/gno.land/r/sys/teams/teams_ownable.gno @@ -14,21 +14,21 @@ type OwnableAccessController struct { DisableAddPackage bool } -func NewOwnableTeam(ownable *ownable.Ownable) *OwnableAccessController { +func NewOwnableAccessController(ownable *ownable.Ownable) *OwnableAccessController { return &OwnableAccessController{Ownable: ownable} } -func (o *OwnableAccessController) CanPerform(member std.Address, msg Msg) bool { - switch msg.(type) { - case AddMemberMsg: +func (o *OwnableAccessController) CanRun(member std.Address, cmd Cmd) bool { + switch cmd.(type) { + case AddMemberCmd: if o.EnableAddMember { return true } - case RemoveMemberMsg: + case RemoveMemberCmd: if o.EnableRemoveMember { return true } - case AddPackageMsg: + case AddPackageCmd: if o.DisableAddPackage { return false } diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index a8267bad107..9610aa38aeb 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -27,17 +27,17 @@ type MyTeam struct { levels avl.Tree // std.Address -> Level } -func (m *MyTeam) Init() teams.Action { +func (m *MyTeam) Init() teams.Task { caller := std.GetOrigCaller() - return teams.ActionableMsg( + return teams.CreateTaskCmd( // Add caller as member - teams.AddMemberMsg{caller}, + teams.AddMemberCmd{caller}, // Promote caller to level4 - SetLevelMsg{caller, Level4}, + SetLevelCmd{caller, Level4}, ) } -func (m *MyTeam) CanPerform(member std.Address, msg teams.Msg) bool { +func (m *MyTeam) CanRun(member std.Address, cmd teams.Cmd) bool { var level Level if mLevel, ok := m.levels.Get(member.String()); ok { level = mLevel.(Level) @@ -47,45 +47,45 @@ func (m *MyTeam) CanPerform(member std.Address, msg teams.Msg) bool { return level >= target } - // Base action - switch msg.(type) { - case SetLevelMsg: + // Team action + switch cmd.(type) { + case SetLevelCmd: return shouldLevelMinimum(Level4) - case teams.RemoveMemberMsg: + case teams.RemoveMemberCmd: return shouldLevelMinimum(Level3) - case teams.AddMemberMsg: + case teams.AddMemberCmd: return shouldLevelMinimum(Level2) - case teams.AddPackageMsg: + case teams.AddPackageCmd: return shouldLevelMinimum(Level1) } return false } -func (m *MyTeam) Update(team *teams.Team, msg teams.Msg) teams.Action { - switch typ := msg.(type) { - case SetLevelMsg: - return teams.Actionable(func(_ *teams.Team) teams.Msg { +func (m *MyTeam) ApplyUpdate(team *teams.Team, cmd teams.Cmd) teams.Task { + switch typ := cmd.(type) { + case SetLevelCmd: + return teams.CreateTask(func(_ *teams.Team) teams.Cmd { mkey := typ.Member.String() m.levels.Set(mkey, typ.Level) return nil }) - case teams.AddMemberMsg: - return teams.ChainActions( + case teams.AddMemberCmd: + return teams.ChainTasks( // Add a new member - teams.AddMemberAction(typ.Member), + teams.AddMemberTask(typ.Member), // Promote it to level 1 - teams.ActionableMsg(SetLevelMsg{ + teams.CreateTaskCmd(SetLevelCmd{ Member: typ.Member, Level: Level1, }), ) } - return team.PerformDefault(msg) + return team.ApplyDefault(cmd) } -type SetLevelMsg struct { +type SetLevelCmd struct { Member std.Address Level } @@ -98,7 +98,7 @@ func (m *MyTeam) GetLevel(member std.Address) Level { } func (m *MyTeam) SetLevel(member std.Address, level Level) { - m.Team.Perform(SetLevelMsg{ + m.Team.Run(SetLevelCmd{ Member: member, Level: level, }) @@ -111,58 +111,38 @@ func init() { } func main() { + // // Setup team address + // alice := testutils.TestAddress("alice") + // Setup user for test alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") - println("myteam can add a package:", myteam.CanPerform(alice, teams.AddPackageMsg{})) + println("myteam can add a package:", myteam.CanAddPackage(alice, "")) // Register alice to the team myteam.AddMember(alice) - println("alice is member: ", myteam.HasMember(alice)) - println("alice is level 1: ", myteam.GetLevel(alice) == Level1) - - // Should be able to add a package on level1 - println("alice can add package:", myteam.CanPerform(alice, teams.AddPackageMsg{})) - // Should be able to add a package on level1 - println("bob cannot add package:", myteam.CanPerform(bob, teams.AddPackageMsg{})) + println("alice is member:", myteam.HasMember(alice)) + println("alice is level_1:", myteam.GetLevel(alice) == Level1) + println("alice can add package:", myteam.CanAddPackage(alice, "")) + println("bob cannot add package:", myteam.CanAddPackage(bob, "")) - // Should not be able to add a member on level1 - println("alice cannot add bob as member:", - myteam.CanPerform(alice, teams.AddMemberMsg{ - Member: bob, - })) + // Alice should not be able to add a member on level1 + println("as level_1, alice cannot add bob as member:", myteam.CanAddMember(alice, bob)) // Update alice to Level4 myteam.SetLevel(alice, Level4) - println("alice is level 4: ", myteam.GetLevel(alice) == Level4) - println("alice can add bob as member:", - myteam.CanPerform(alice, teams.AddMemberMsg{ - Member: bob, - })) + println("alice is level_4:", myteam.GetLevel(alice) == Level4) + println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) // Set caller to alice std.TestSetOrigCaller(alice) // alice add member bob - println("adding bob") + println("alice add bob as member") myteam.AddMember(bob) - println("bob is member: ", myteam.HasMember(alice)) - println("bob is level 1: ", myteam.GetLevel(alice) == Level1) - - // Check if alice can perform add package - // println(teams.CanPerform(myTeamUser, alice, teams.AddPackageMsg{})) - - // Set alice as caller - // std.TestSetOrigCaller(alice) - // Try to perform add package - - // println(teams.CanPerform(myTeamUser, alice, teams.AddPackageMsg{})) - - // bob := testutils.TestAddress("bob") - // println(teams.IsRegister(myteam.address)) - // myteam.address = teams.Register(&myteam) - // println(teams.IsRegister(myteam.address)) + println("bob is member:", myteam.HasMember(alice)) + println("bob is level_1:", myteam.GetLevel(alice) == Level1) } diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index da887764c8e..aff65ecda48 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1963,7 +1963,7 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val "native type %s has no method or field %s", dtv.T.String(), path.Name)) default: - panic("should not happen") + panic("should not happen: " + fmt.Sprintf("path: %#v", path)) } } From 6c8fb47e6e322eb508e5f288fc1b62123103a1c6 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:19:35 +0100 Subject: [PATCH 04/18] wip: -- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/cmd.gno | 5 +- examples/gno.land/r/sys/teams/teams.gno | 12 ++--- .../gno.land/r/sys/teams/z_1_filetest.gno | 53 +++++++++++-------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno index a997a1933c0..5549f37341e 100644 --- a/examples/gno.land/r/sys/teams/cmd.gno +++ b/examples/gno.land/r/sys/teams/cmd.gno @@ -6,6 +6,7 @@ import ( ) var ErrAlreadyExist = errors.New("already exist") +var ErrDoesNotExist = errors.New("does not exist") type Cmd interface{} @@ -29,8 +30,8 @@ type RemoveMemberCmd struct { func RemoveMemberTask(member std.Address) Task { return CreateTask(func(t *Team) Cmd { - if t.members.Has(member.String()) { - panic(ErrAlreadyExist) + if !t.members.Has(member.String()) { + panic(ErrDoesNotExist) } t.members.Set(member.String(), struct{}{}) return nil diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index 9039e9cc326..fb6aea7aa7b 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -33,7 +33,7 @@ type Team struct { func (team *Team) Run(cmds ...Cmd) { caller := std.GetOrigCaller() - if !team.IsMember(caller) { + if !team.HasMember(caller) { panic("only member can perform command on team") } @@ -63,21 +63,17 @@ func (team *Team) CanRemoveMember(member, target std.Address) bool { } func (team *Team) HasMember(member std.Address) bool { - return team.members.Has(member.String()) + return member == team.address || team.members.Has(member.String()) } func (team *Team) CanRun(caller std.Address, cmd Cmd) bool { - return !team.IsTeamAddress(caller) && !team.AccessController.CanRun(caller, cmd) + return team.IsTeamAddress(caller) || team.AccessController.CanRun(caller, cmd) } func (team *Team) IsTeamAddress(teamAddr std.Address) bool { return teamAddr == team.address } -func (team *Team) IsMember(member std.Address) bool { - return member == team.address || team.members.Has(member.String()) -} - func (team *Team) ApplyDefault(cmd Cmd) Task { switch typ := cmd.(type) { case AddMemberCmd: @@ -123,7 +119,7 @@ func (team *Team) getTasksForCmds(cmds ...Cmd) []Task { tasks = append(tasks, subTasks...) default: // Assert caller can perform task - if team.CanRun(caller, cmd) { + if !team.CanRun(caller, cmd) { panic("unauthorized") } diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index 9610aa38aeb..3ed61d33390 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -38,25 +38,21 @@ func (m *MyTeam) Init() teams.Task { } func (m *MyTeam) CanRun(member std.Address, cmd teams.Cmd) bool { - var level Level - if mLevel, ok := m.levels.Get(member.String()); ok { - level = mLevel.(Level) - } - - shouldLevelMinimum := func(target Level) bool { + level := m.GetLevel(member) + shouldBeLevelMinimum := func(target Level) bool { return level >= target } // Team action switch cmd.(type) { case SetLevelCmd: - return shouldLevelMinimum(Level4) + return shouldBeLevelMinimum(Level4) case teams.RemoveMemberCmd: - return shouldLevelMinimum(Level3) + return shouldBeLevelMinimum(Level3) case teams.AddMemberCmd: - return shouldLevelMinimum(Level2) + return shouldBeLevelMinimum(Level2) case teams.AddPackageCmd: - return shouldLevelMinimum(Level1) + return shouldBeLevelMinimum(Level1) } return false @@ -106,43 +102,54 @@ func (m *MyTeam) SetLevel(member std.Address, level Level) { var myteam MyTeam -func init() { - myteam.Team = teams.Register(&myteam) -} - func main() { - // // Setup team address - // alice := testutils.TestAddress("alice") + // Setup team user address + myteamUser := testutils.TestAddress("myteamUser") + std.TestSetOrigCaller(myteamUser) + + println(" -> register team") + myteam.Team = teams.Register(&myteam) + println("myteamUser is member:", myteam.HasMember(myteamUser)) + println("myteamUser is level_4:", myteam.GetLevel(myteamUser) == Level4) + println("myteamUser can add package:", myteam.CanAddPackage(myteamUser, "")) - // Setup user for test + // Setup test users alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") - println("myteam can add a package:", myteam.CanAddPackage(alice, "")) + println("alice cannot add a package:", !myteam.CanAddPackage(alice, "")) // Register alice to the team + println(" -> adding alice as a member") myteam.AddMember(alice) println("alice is member:", myteam.HasMember(alice)) println("alice is level_1:", myteam.GetLevel(alice) == Level1) println("alice can add package:", myteam.CanAddPackage(alice, "")) - println("bob cannot add package:", myteam.CanAddPackage(bob, "")) + println("bob cannot add package:", !myteam.CanAddPackage(bob, "")) // Alice should not be able to add a member on level1 - println("as level_1, alice cannot add bob as member:", myteam.CanAddMember(alice, bob)) + println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) // Update alice to Level4 + println(" -> setting alice to level 4") myteam.SetLevel(alice, Level4) println("alice is level_4:", myteam.GetLevel(alice) == Level4) println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) // Set caller to alice + println(" -> setting alice as origin caller") std.TestSetOrigCaller(alice) // alice add member bob - println("alice add bob as member") + println(" -> alice add bob as member") myteam.AddMember(bob) - println("bob is member:", myteam.HasMember(alice)) - println("bob is level_1:", myteam.GetLevel(alice) == Level1) + println("bob is member:", myteam.HasMember(bob)) + println("bob is level_1:", myteam.GetLevel(bob) == Level1) + println("bob can add package:", myteam.CanAddPackage(bob, "")) + + println(" -> removing bob") + myteam.RemoveMember(bob) + println("bob is not member:", !myteam.HasMember(bob)) } From 031d1fa0f35c8db2c3e354ab02290df8d003875b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:21:27 +0100 Subject: [PATCH 05/18] wip: cleanup teams api Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/cmd.gno | 65 +++- examples/gno.land/r/sys/teams/task.gno | 18 +- examples/gno.land/r/sys/teams/teams.gno | 312 +++++++++++++----- .../gno.land/r/sys/teams/teams_ownable.gno | 40 ++- .../gno.land/r/sys/teams/z_1_filetest.gno | 153 +-------- .../gno.land/r/sys/teams/z_9_filetest.gno | 179 ++++++++++ 6 files changed, 527 insertions(+), 240 deletions(-) create mode 100644 examples/gno.land/r/sys/teams/z_9_filetest.gno diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno index 5549f37341e..3a5a3542595 100644 --- a/examples/gno.land/r/sys/teams/cmd.gno +++ b/examples/gno.land/r/sys/teams/cmd.gno @@ -3,19 +3,49 @@ package teams import ( "errors" "std" + "strings" ) -var ErrAlreadyExist = errors.New("already exist") +var ErrAlreadyExist = errors.New("already exists") var ErrDoesNotExist = errors.New("does not exist") -type Cmd interface{} +// Cmd represents a command that can be executed on a team. +// Each command should implement the Name method to provide a string identifier. +type Cmd interface { + Name() string +} + +type Cmds []Cmd + +func (cmds Cmds) Name() string { + var str strings.Builder + str.WriteRune('[') + for i, cmd := range cmds { + if i > 0 { + str.WriteRune(',') + } + + str.WriteString(cmd.Name()) + } + str.WriteRune(']') + return str.String() +} +// AddMemberCmd represents a command to add a member to the team. type AddMemberCmd struct { Member std.Address } +func (cmd AddMemberCmd) Name() string { return "AddMember" } + +// AddMemberTask creates a task to add a member to the team. func AddMemberTask(member std.Address) Task { return CreateTask(func(t *Team) Cmd { + // Cannot add team address as a member + if t.IsTeamAddress(member) { + panic("cannot add team address as a member") + } + if t.members.Has(member.String()) { panic(ErrAlreadyExist) } @@ -24,20 +54,49 @@ func AddMemberTask(member std.Address) Task { }) } +// RemoveMemberCmd represents a command to remove a member from the team. type RemoveMemberCmd struct { Member std.Address } +func (cmd RemoveMemberCmd) Name() string { return "RemoveMember" } + +// RemoveMemberTask creates a task to remove a member from the team. func RemoveMemberTask(member std.Address) Task { return CreateTask(func(t *Team) Cmd { if !t.members.Has(member.String()) { panic(ErrDoesNotExist) } - t.members.Set(member.String(), struct{}{}) + t.members.Remove(member.String()) return nil }) } +// UpdateAccessControllerCmd represents a command to update the team's access controller. +type UpdateAccessControllerCmd struct { + AccessController +} + +func (cmd UpdateAccessControllerCmd) Name() string { return "UpdateAccessController" } + +// UpdateAccessControllerTask creates a task to update the team's access controller. +func UpdateAccessControllerTask(ac AccessController) Task { + return CreateTask(func(t *Team) Cmd { + t.AccessController = ac + return nil + }) +} + +// BurnTeamAddressCmd represents a command to burn the team's address. +type BurnTeamAddressCmd struct{} + +func (cmd BurnTeamAddressCmd) Name() string { return "BurnTeamAddress" } + +var BurnTeamAddressTask = CreateTaskCmd(BurnTeamAddressCmd{}) + +// AddPackageCmd represents a command to add a package to the team. type AddPackageCmd struct { Path string } + +func (cmd AddPackageCmd) Name() string { return "AddPackage" } diff --git a/examples/gno.land/r/sys/teams/task.gno b/examples/gno.land/r/sys/teams/task.gno index f6f3561ee2e..1ea1020f798 100644 --- a/examples/gno.land/r/sys/teams/task.gno +++ b/examples/gno.land/r/sys/teams/task.gno @@ -1,7 +1,16 @@ package teams +// TaskFunc defines a function type that takes a Team and returns a Cmd. +// It represents the executable logic that can be performed on a team. type TaskFunc func(t *Team) Cmd +// Task represents a unit of work that can be executed to apply a command. +// Tasks encapsulate the execution logic of commands, ensuring that operations +// are performed with the correct permissions and in a specified order. +// +// Tasks are used to execute commands that modify the team's state, ensuring +// that these modifications adhere to the permissions set by the access control +// mechanisms and are executed in a controlled sequence. type Task interface { call(t *Team) Cmd } @@ -14,6 +23,7 @@ func (a task) call(t *Team) Cmd { return a.actionFunc(t) } +// CreateTaskCmd creates a Task from one or more commands. func CreateTaskCmd(cmd ...Cmd) Task { switch len(cmd) { case 0: @@ -23,6 +33,7 @@ func CreateTaskCmd(cmd ...Cmd) Task { return cmd[0] }) default: + // Handle multiple commands } fns := make([]TaskFunc, len(cmd)) @@ -34,6 +45,7 @@ func CreateTaskCmd(cmd ...Cmd) Task { return CreateTask(fns...) } +// CreateTask creates a Task from one or more TaskFuncs. func CreateTask(fn ...TaskFunc) Task { switch len(fn) { case 0: @@ -41,6 +53,7 @@ func CreateTask(fn ...TaskFunc) Task { case 1: return &task{actionFunc: fn[0]} default: + // Handle multiple functions } actions := make([]Task, len(fn)) @@ -50,6 +63,8 @@ func CreateTask(fn ...TaskFunc) Task { return ChainTasks(actions...) } +// ChainTasks creates a single Task that executes a series of tasks in sequence. +// It combines multiple tasks into one. func ChainTasks(actions ...Task) Task { switch len(actions) { case 0: @@ -57,6 +72,7 @@ func ChainTasks(actions ...Task) Task { case 1: return actions[0] default: + // Handle chaining of multiple tasks } return CreateTask(func(t *Team) Cmd { @@ -64,6 +80,6 @@ func ChainTasks(actions ...Task) Task { for i, action := range actions { cmds[i] = action.call(t) } - return cmds + return Cmds(cmds) }) } diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index fb6aea7aa7b..cfbc85ffa96 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -7,45 +7,198 @@ import ( "gno.land/p/demo/avl" ) +// AccessController defines the interface for controlling access to team commands. +// Implementations should define the logic to determine if a member can run a specific command. type AccessController interface { + // CanRun determines if a member is authorized to execute a specific command. CanRun(member std.Address, cmd Cmd) bool } +// Lifecycle defines the interface for managing the lifecycle of a team. +// It provides methods for initializing a team and applying updates through commands. +// Implementations of this interface should define how a team is set up initially +// and how it responds to changes over time. type Lifecycle interface { + // Init initializes the team with a series of commands. + // It returns a Task that encapsulates the initialization logic. + // + // The Init method is called during the creation of a team to perform + // initial setup actions such as adding founding members or configuring + // initial settings. Notably, this method operates with elevated privileges, + // meaning it does not enforce access control checks via the AccessController. + // This allows the initial setup to bypass usual restrictions, enabling + // foundational configuration without member-specific constraints. + // + // Example: + // func (m *MyTeam) Init() teams.Task { + // return teams.CreateTaskCmd( + // teams.AddMemberCmd{myteam.MyUser}, // Add MyUser as a member + // SetLevelCmd{myteam.MyUser, Level4}, // Set MyUser's level to Level4 + // ) + // } Init() Task - ApplyUpdate(team *Team, cmd Cmd) Task + + // ApplyUpdate applies an update to the team based on a given command. + // It returns a Task that represents the execution of this update. + // + // The ApplyUpdate method is used to modify the team's state in response + // to commands such as adding or removing members, or changing member roles. + // Unlike Init, this method enforces access control checks using the + // AccessController. Before executing a command, ApplyUpdate ensures that + // the member issuing the command has the necessary permissions, maintaining + // the security and integrity of team operations. + // + // Example: + // func (m *MyTeam) ApplyUpdate(cmd teams.Cmd) teams.Task { + // switch typ := cmd.(type) { + // case SetLevelCmd: + // return teams.CreateTask(func(_ *teams.Team) teams.Cmd { + // m.levels.Set(typ.Member.String(), typ.Level) + // return nil + // }) + // } + // return team.ApplyDefault(cmd) + // } + ApplyUpdate(cmd Cmd) Task } +// ITeam combines the AccessController and Lifecycle interfaces to define a complete +// team interface. It ensures that a team has both access control and lifecycle +// management capabilities. type ITeam interface { AccessController Lifecycle } -var teams avl.Tree // std.Address -> Team +var teams avl.Tree // std.Address -> *Team type Team struct { AccessController Lifecycle - address std.Address - members avl.Tree // std.Address -> void + burned bool + address std.Address // Origin address of the team + members avl.Tree // std.Address -> void + + // internal + isUpdating bool +} + +// Realm of the form `xxx.xx/r//home` +var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) + +// Register creates and registers a new team in the `r/sys/teams` realm, allowing a registered +// user to transform into a team by publishing a contract that registers members in a registry. +// +// The `Register` function verifies that the caller is a valid home path (`r//home`), +// using `` as the team name, and ensures that the caller is not already registered as a team. +func Register(iteam ITeam) *Team { + caller := std.GetOrigCaller() + realm := std.PrevRealm() + + // Check if caller is not already registered as a team + if teams.Has(caller.String()) { + panic("team already registered: " + caller) + } + + // Check if origin caller is a home path + if !reHomeRealm.MatchString(realm.PkgPath()) { + panic("cannot register a team outside a home realm") + } + + // Then initilize the team + team := &Team{ + address: caller, + Lifecycle: iteam, + AccessController: iteam, + } + + // Assert that team implementation correctly uses fallback + // XXX: do we want this? + // It asserts that caller has a minimal implementation of its team + assertDefaultUpdate(team) + + if initTask := team.Init(); initTask != nil { + team.performTasks(caller, initTask) + } + + // All set, register the team + teams.Set(caller.String(), team) + return team } +// Run executes a series of commands on the team, ensuring that only authorized +// members can perform these operations. It translates commands into tasks and +// executes them to update the team's state. +// +// This is the entrypoint to execute command on the team. +// +// Flow: +// +// 1. **Caller Verification**: The method retrieves the original caller's address +// and checks if they are a member of the team. If not, it panics, preventing +// unauthorized access. +// +// 2. **Command Translation**: The method translates the provided commands into +// tasks. This involves checking if each command is authorized for execution +// by the caller and preparing the corresponding tasks. +// +// 3. **Task Execution**: The method executes the tasks, ensuring that each +// task is executed in sequence, effectively updating the team's state. func (team *Team) Run(cmds ...Cmd) { + if !team.IsRegister() { + panic("team is not register") + } + caller := std.GetOrigCaller() - if !team.HasMember(caller) { - panic("only member can perform command on team") + if !team.IsTeamAddress(caller) && !team.HasMember(caller) { + panic("only members / team address can perform commands on the team") + } + + // Get tasks for the given cmds + tasks := team.getTasksForCmds(caller, cmds...) + // Perform tasks + team.performTasks(caller, tasks...) +} + +// Default Access Controller + +func (team *Team) CanRun(member std.Address, cmd Cmd) bool { + if !team.burned && team.IsTeamAddress(member) { + return true } - // get actions for the given cmds - actions := team.getTasksForCmds(cmds...) - team.performTasks(actions...) + return team.AccessController != nil && team.AccessController.CanRun(member, cmd) +} + +// Default Lifecycle implementation + +func (team *Team) Init() Task { + if team.Lifecycle != nil { + return team.Lifecycle.Init() + } + + return nil +} + +func (team *Team) ApplyUpdate(cmd Cmd) Task { + if team.Lifecycle != nil { + return team.Lifecycle.ApplyUpdate(cmd) + } + + return ApplyDefault(cmd) } func (team *Team) CanAddPackage(member std.Address, path string) bool { return team.CanRun(member, AddPackageCmd{Path: path}) } +// BurnTeamAddress prevent the team address from managing the team, leaving it to the members. +// WARNING: This is irreversible +func (team *Team) BurnTeamAddress() { + team.Run(BurnTeamAddressCmd{}) +} + func (team *Team) AddMember(member std.Address) { team.Run(AddMemberCmd{Member: member}) } @@ -63,135 +216,118 @@ func (team *Team) CanRemoveMember(member, target std.Address) bool { } func (team *Team) HasMember(member std.Address) bool { - return member == team.address || team.members.Has(member.String()) -} - -func (team *Team) CanRun(caller std.Address, cmd Cmd) bool { - return team.IsTeamAddress(caller) || team.AccessController.CanRun(caller, cmd) + return team.members.Has(member.String()) } func (team *Team) IsTeamAddress(teamAddr std.Address) bool { return teamAddr == team.address } -func (team *Team) ApplyDefault(cmd Cmd) Task { +func (team *Team) IsRegister() bool { + return team.address != "" +} + +func (team *Team) Address() std.Address { + return team.address +} + +func ApplyDefault(cmd Cmd) Task { switch typ := cmd.(type) { + case assertDefaultUpdateCmd: + typ.assert = true + return CreateTaskCmd(typ) case AddMemberCmd: return AddMemberTask(typ.Member) case RemoveMemberCmd: return RemoveMemberTask(typ.Member) - case AddPackageCmd: // Do nothing - - case assertRunDefaultCmd: - typ.assert = true - return CreateTaskCmd(typ) + case AddPackageCmd: // XXX: + return nil + case UpdateAccessControllerCmd: + // this commands are not supported by default } - return nil + panic("command not supported") } -func (team *Team) performTasks(tasks ...Task) { +func IsRegister(teamAddr std.Address) bool { + return teams.Has(teamAddr.String()) +} + +func (team *Team) performTasks(caller std.Address, tasks ...Task) { + if team.isUpdating { + panic(`cannot perform task while updating, ensure returning the task instead`) + } + var task Task for len(tasks) > 0 { - task, tasks = tasks[0], tasks[1:] // shift task + task, tasks = tasks[0], tasks[1:] // Shift task if task == nil { - continue // skip empty task + continue // Skip empty task } if cmd := task.call(team); cmd != nil { - nextTasks := team.getTasksForCmds(cmd) + nextTasks := team.getTasksForCmds(caller, cmd) tasks = append(nextTasks, tasks...) } } } -func (team *Team) getTasksForCmds(cmds ...Cmd) []Task { - caller := std.GetOrigCaller() +func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { + team.isUpdating = true + defer func() { team.isUpdating = false }() + tasks := make([]Task, 0, len(cmds)) for _, cmd := range cmds { if cmd == nil { - continue // skip empty cmd + continue // Skip empty cmd } switch typ := cmd.(type) { - case []Cmd: - subTasks := team.getTasksForCmds(typ...) + case Cmds: + subTasks := team.getTasksForCmds(caller, typ...) tasks = append(tasks, subTasks...) + case BurnTeamAddressCmd: // special case, can't be ignored + if team.burned { + panic("already burned") + } + + if !team.IsTeamAddress(caller) { + panic("only team address can burn") + } + + tasks = append(tasks, CreateTask(func(t *Team) Cmd { + team.burned = true + return nil + })) + + // XXX: do we fallthrough here? can be usefull for + // people to take some action but can also let people + // prevent burning by using `panic` default: // Assert caller can perform task if !team.CanRun(caller, cmd) { - panic("unauthorized") + panic("unauthorized command: " + "[" + cmd.Name() + "]") } // Prepare task - tasks = append(tasks, team.Lifecycle.ApplyUpdate(team, cmd)) + tasks = append(tasks, team.ApplyUpdate(cmd)) } } return tasks } -type unlimitedAC struct{} - -func (unlimitedAC) CanRun(member std.Address, cmd Cmd) bool { - return true -} - -// realm of the form `xxx.xx/r//home` -var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) - -func Register(iteam ITeam) *Team { - caller := std.GetOrigCaller() - realm := std.PrevRealm() - - // First lets check the realm is valid - if iteam == nil { - panic("team cannot be nil") - } - - // Check if origin caller is an home path - if !reHomeRealm.MatchString(realm.PkgPath()) { - panic("cannot register a team outside an home realm") - } - - // Check if caller is not already registerer as a team - if teams.Has(caller.String()) { - panic("team already registered: " + caller) - } - - // Then initilize the team - team := &Team{address: caller, Lifecycle: iteam} - - // Assert that team implementation correctly use fallback - // XXX: do we want this ? - // > it assert that caller have a minimal implementation of his team - team.assertRunDefault() - - if initTask := iteam.Init(); initTask != nil { - // init is performed using an unlimited ac - team.AccessController = unlimitedAC{} - team.performTasks(initTask) - } - - // Once done, apply the provided implementation ac - team.AccessController = iteam - teams.Set(caller.String(), team) - return team -} - -func IsRegister(teamAddr std.Address) bool { - return teams.Has(teamAddr.String()) -} +type assertDefaultUpdateCmd struct{ assert bool } -type assertRunDefaultCmd struct{ assert bool } +func (assertDefaultUpdateCmd) Name() string { return "assertDefaultUpdateCmd" } -func (team *Team) assertRunDefault() { - task := team.Lifecycle.ApplyUpdate(team, assertRunDefaultCmd{}) +func assertDefaultUpdate(team *Team) { + task := team.ApplyUpdate(assertDefaultUpdateCmd{}) cmd := task.call(team) - if assertCmd, ok := cmd.(assertRunDefaultCmd); ok { + if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok { if assertCmd.assert { return } } - panic(`make sure that team implementation handle team update fallback`) + panic("ensure that team implementation handles team update fallback") } diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno index c0e6bda089a..7b024f2411c 100644 --- a/examples/gno.land/r/sys/teams/teams_ownable.gno +++ b/examples/gno.land/r/sys/teams/teams_ownable.gno @@ -6,6 +6,28 @@ import ( "gno.land/p/demo/ownable" ) +type OwnableTeam struct { + *OwnableAccessController +} + +func NewOwnableTeam(owner *ownable.Ownable) ITeam { + return &OwnableTeam{ + OwnableAccessController: NewOwnableAccessController(owner), + } +} + +func (o *OwnableTeam) Init() Task { + return ChainTasks( + AddMemberTask(o.Owner()), + // burn team address, so only owner can control the team + BurnTeamAddressTask, + ) +} + +func (o *OwnableTeam) ApplyUpdate(cmd Cmd) Task { + return ApplyDefault(cmd) +} + type OwnableAccessController struct { *ownable.Ownable @@ -19,20 +41,18 @@ func NewOwnableAccessController(ownable *ownable.Ownable) *OwnableAccessControll } func (o *OwnableAccessController) CanRun(member std.Address, cmd Cmd) bool { + if o.Ownable.Owner() == member { // All mighty owner + return true + } + switch cmd.(type) { case AddMemberCmd: - if o.EnableAddMember { - return true - } + return o.EnableAddMember case RemoveMemberCmd: - if o.EnableRemoveMember { - return true - } + return o.EnableRemoveMember case AddPackageCmd: - if o.DisableAddPackage { - return false - } + return !o.DisableAddPackage } - return o.Ownable.Owner() == member + return false } diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index 3ed61d33390..705423e3cca 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -1,155 +1,32 @@ // PKGPATH: gno.land/r/myteam/home + +// This the most minimal usage of team package home import ( - "std" - - "gno.land/p/demo/avl" "gno.land/p/demo/testutils" "gno.land/r/sys/teams" ) -// var myteam *Team - -type Level int - -const ( - LevelUnknown Level = iota // lowest - Level1 - Level2 - Level3 - Level4 -) - -type MyTeam struct { - *teams.Team - address std.Address - levels avl.Tree // std.Address -> Level -} - -func (m *MyTeam) Init() teams.Task { - caller := std.GetOrigCaller() - return teams.CreateTaskCmd( - // Add caller as member - teams.AddMemberCmd{caller}, - // Promote caller to level4 - SetLevelCmd{caller, Level4}, - ) -} - -func (m *MyTeam) CanRun(member std.Address, cmd teams.Cmd) bool { - level := m.GetLevel(member) - shouldBeLevelMinimum := func(target Level) bool { - return level >= target - } - - // Team action - switch cmd.(type) { - case SetLevelCmd: - return shouldBeLevelMinimum(Level4) - case teams.RemoveMemberCmd: - return shouldBeLevelMinimum(Level3) - case teams.AddMemberCmd: - return shouldBeLevelMinimum(Level2) - case teams.AddPackageCmd: - return shouldBeLevelMinimum(Level1) - } - - return false -} - -func (m *MyTeam) ApplyUpdate(team *teams.Team, cmd teams.Cmd) teams.Task { - switch typ := cmd.(type) { - case SetLevelCmd: - return teams.CreateTask(func(_ *teams.Team) teams.Cmd { - mkey := typ.Member.String() - m.levels.Set(mkey, typ.Level) - return nil - }) - case teams.AddMemberCmd: - return teams.ChainTasks( - // Add a new member - teams.AddMemberTask(typ.Member), - // Promote it to level 1 - teams.CreateTaskCmd(SetLevelCmd{ - Member: typ.Member, - Level: Level1, - }), - ) - } - - return team.ApplyDefault(cmd) -} - -type SetLevelCmd struct { - Member std.Address - Level -} +var myteam *teams.Team -func (m *MyTeam) GetLevel(member std.Address) Level { - if level, ok := m.levels.Get(member.String()); ok { - return level.(Level) - } - return LevelUnknown -} - -func (m *MyTeam) SetLevel(member std.Address, level Level) { - m.Team.Run(SetLevelCmd{ - Member: member, - Level: level, - }) +// var myteam *Team +func init() { + myteam = teams.Register(nil) } -var myteam MyTeam - func main() { - // Setup team user address - myteamUser := testutils.TestAddress("myteamUser") - std.TestSetOrigCaller(myteamUser) - - println(" -> register team") - myteam.Team = teams.Register(&myteam) - println("myteamUser is member:", myteam.HasMember(myteamUser)) - println("myteamUser is level_4:", myteam.GetLevel(myteamUser) == Level4) - println("myteamUser can add package:", myteam.CanAddPackage(myteamUser, "")) - - // Setup test users alice := testutils.TestAddress("alice") - bob := testutils.TestAddress("bob") + teamAddress := myteam.Address() - println("alice cannot add a package:", !myteam.CanAddPackage(alice, "")) + // Setup team user address + println("team address is team address:", myteam.IsTeamAddress(teamAddress)) + println("team address is not member:", !myteam.HasMember(teamAddress)) + println("team address can add package:", myteam.CanAddPackage(teamAddress, "")) + println("team address can add member alice:", myteam.CanAddMember(teamAddress, alice)) - // Register alice to the team - println(" -> adding alice as a member") + println("alice is not member:", !myteam.HasMember(alice)) + println(" -> adding alice as member") myteam.AddMember(alice) - println("alice is member:", myteam.HasMember(alice)) - println("alice is level_1:", myteam.GetLevel(alice) == Level1) - println("alice can add package:", myteam.CanAddPackage(alice, "")) - println("bob cannot add package:", !myteam.CanAddPackage(bob, "")) - - // Alice should not be able to add a member on level1 - println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) - - // Update alice to Level4 - println(" -> setting alice to level 4") - myteam.SetLevel(alice, Level4) - println("alice is level_4:", myteam.GetLevel(alice) == Level4) - println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) - - // Set caller to alice - println(" -> setting alice as origin caller") - std.TestSetOrigCaller(alice) - - // alice add member bob - println(" -> alice add bob as member") - myteam.AddMember(bob) - - println("bob is member:", myteam.HasMember(bob)) - println("bob is level_1:", myteam.GetLevel(bob) == Level1) - println("bob can add package:", myteam.CanAddPackage(bob, "")) - - println(" -> removing bob") - myteam.RemoveMember(bob) - println("bob is not member:", !myteam.HasMember(bob)) - + println("alice is now a member:", myteam.HasMember(alice)) } diff --git a/examples/gno.land/r/sys/teams/z_9_filetest.gno b/examples/gno.land/r/sys/teams/z_9_filetest.gno new file mode 100644 index 00000000000..75c41c1e0d2 --- /dev/null +++ b/examples/gno.land/r/sys/teams/z_9_filetest.gno @@ -0,0 +1,179 @@ +// PKGPATH: gno.land/r/myteam/home + +// This is an example of a more advanced usage of team +package home + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" + "gno.land/r/sys/teams" +) + +// var myteam *Team + +type Level int + +const ( + LevelUnknown Level = iota // lowest + Level1 + Level2 + Level3 + Level4 +) + +type MyTeam struct { + *teams.Team + address std.Address + levels avl.Tree // std.Address -> Level +} + +func (m *MyTeam) Init() teams.Task { + return nil +} + +func (m *MyTeam) CanRun(member std.Address, cmd teams.Cmd) bool { + level := m.GetLevel(member) + shouldBeLevelMinimum := func(target Level) bool { + return level >= target + } + + // Team action + switch typ := cmd.(type) { + case SetLevelCmd: + // Can only set level on inferior level + return level > typ.Level + case teams.RemoveMemberCmd: + return level >= Level3 + case teams.AddMemberCmd: + return level >= Level2 + case teams.AddPackageCmd: + return level >= Level1 + } + + return false +} + +type SetLevelCmd struct { + Member std.Address + Level +} + +func (SetLevelCmd) Name() string { return "SetLevel" } + +func (m *MyTeam) ApplyUpdate(cmd teams.Cmd) teams.Task { + switch typ := cmd.(type) { + case SetLevelCmd: + return teams.CreateTask(func(_ *teams.Team) teams.Cmd { + mkey := typ.Member.String() + m.levels.Set(mkey, typ.Level) + return nil + }) + case teams.AddMemberCmd: + return teams.ChainTasks( + // Add a new member + teams.AddMemberTask(typ.Member), + // Promote it to level 1 + teams.CreateTaskCmd(SetLevelCmd{ + Member: typ.Member, + Level: Level1, + }), + ) + case teams.RemoveMemberCmd: + return teams.ChainTasks( + // Add a new member + teams.RemoveMemberTask(typ.Member), + // Promote it to level 1 + teams.CreateTaskCmd(SetLevelCmd{ + Member: typ.Member, + Level: LevelUnknown, + }), + ) + + } + + return teams.ApplyDefault(cmd) +} + +func (m *MyTeam) GetLevel(member std.Address) Level { + if level, ok := m.levels.Get(member.String()); ok { + return level.(Level) + } + return LevelUnknown +} + +func (m *MyTeam) SetLevel(member std.Address, level Level) { + m.Team.Run(SetLevelCmd{ + Member: member, + Level: level, + }) +} + +var myteam MyTeam + +func init() { + // inherit all methods from Team + myteam.Team = teams.Register(&myteam) +} + +func main() { + myteamUser := myteam.Address() + println(myteamUser) + + // Setup team user address + println(" -> register myteam") + + println("myteamUser is not member:", !myteam.HasMember(myteamUser)) + println("myteamUser has not level:", myteam.GetLevel(myteamUser) == LevelUnknown) + println("myteamUser can add package:", myteam.CanAddPackage(myteamUser, "")) + + // Setup test users + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + + println("alice cannot add a package:", !myteam.CanAddPackage(alice, "")) + + // Register alice to the team + println(" -> adding alice as a member") + myteam.AddMember(alice) + println("alice is member:", myteam.HasMember(alice)) + println("alice is level_1:", myteam.GetLevel(alice) == Level1) + println("alice can add package:", myteam.CanAddPackage(alice, "")) + println("bob cannot add package:", !myteam.CanAddPackage(bob, "")) + + // Alice should not be able to add a member on level1 + println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) + + // Update alice to Level4 + println(" -> setting alice to level 4") + myteam.SetLevel(alice, Level4) + println("alice is level_4:", myteam.GetLevel(alice) == Level4) + println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) + + println(" -> burn team address") + myteam.BurnTeamAddress() + println("myteamUser is not member:", myteam.HasMember(myteamUser)) + println("myteamUser is level_Unknown:", myteam.GetLevel(myteamUser) == LevelUnknown) + println("myteamUser cannot add package:", myteam.CanAddPackage(myteamUser, "")) + + println("alice is still level_4:", myteam.GetLevel(alice) == Level4) + println("alice can still add bob as member:", myteam.CanAddMember(alice, bob)) + + // Set caller to alice + println(" -> setting alice as origin caller") + std.TestSetOrigCaller(alice) + + // alice add member bob + println(" -> alice add bob as member") + myteam.AddMember(bob) + + println("bob is member:", myteam.HasMember(bob)) + println("bob is level_1:", myteam.GetLevel(bob) == Level1) + println("bob can add package:", myteam.CanAddPackage(bob, "")) + + println(" -> removing bob") + myteam.RemoveMember(bob) + println("bob is not member:", !myteam.HasMember(bob)) + +} From 2157e23acd19ba05aebd0ed4e126a8d84b02afac Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:06:12 +0100 Subject: [PATCH 06/18] wip: cleanup & test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/cmd.gno | 17 ++ examples/gno.land/r/sys/teams/team.gno | 268 ++++++++++++++++++ examples/gno.land/r/sys/teams/teams.gno | 226 +-------------- .../gno.land/r/sys/teams/teams_ownable.gno | 1 + .../gno.land/r/sys/teams/z_1_filetest.gno | 2 +- .../gno.land/r/sys/teams/z_9_filetest.gno | 50 +++- examples/gno.land/r/sys/users/verify.gno | 7 + .../integration/testdata/register_team.txtar | 98 +++++++ 8 files changed, 441 insertions(+), 228 deletions(-) create mode 100644 examples/gno.land/r/sys/teams/team.gno create mode 100644 gno.land/pkg/integration/testdata/register_team.txtar diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno index 3a5a3542595..b3bd878555b 100644 --- a/examples/gno.land/r/sys/teams/cmd.gno +++ b/examples/gno.land/r/sys/teams/cmd.gno @@ -72,6 +72,8 @@ func RemoveMemberTask(member std.Address) Task { }) } +// The command bellow should be use with precaution + // UpdateAccessControllerCmd represents a command to update the team's access controller. type UpdateAccessControllerCmd struct { AccessController @@ -87,6 +89,21 @@ func UpdateAccessControllerTask(ac AccessController) Task { }) } +// UpdateLifecycleCmd represents a command to update the team's access controller. +type UpdateLifecycleCmd struct { + Lifecycle +} + +func (cmd UpdateLifecycleCmd) Name() string { return "UpdateLifecycle" } + +// UpdateLifecycleTask creates a task to update the team's access controller. +func UpdateLifecycleTask(ac Lifecycle) Task { + return CreateTask(func(t *Team) Cmd { + t.Lifecycle = ac + return nil + }) +} + // BurnTeamAddressCmd represents a command to burn the team's address. type BurnTeamAddressCmd struct{} diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno new file mode 100644 index 00000000000..cfb5c912870 --- /dev/null +++ b/examples/gno.land/r/sys/teams/team.gno @@ -0,0 +1,268 @@ +package teams + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Team struct { + AccessController + Lifecycle + + address std.Address // Origin address of the team + members avl.Tree // std.Address -> void + burned bool + + // internal + isUpdating bool +} + +// Run executes a series of commands on the team, ensuring that only authorized +// members can perform these operations. It translates commands into tasks and +// executes them to update the team's state. +// +// This is the entrypoint to execute command on the team. +// +// Flow: +// +// 1. **Caller Verification**: The method retrieves the original caller's address +// and checks if they are a member of the team. If not, it panics, preventing +// unauthorized access. +// +// 2. **Command Translation**: The method translates the provided commands into +// tasks. This involves checking if each command is authorized for execution +// by the caller and preparing the corresponding tasks. +// +// 3. **Task Execution**: The method executes the tasks, ensuring that each +// task is executed in sequence, effectively updating the team's state. +func (team *Team) Run(cmds ...Cmd) { + if !team.IsRegister() { + panic("team is not register") + } + + caller := std.GetOrigCaller() + if !team.IsTeamAddress(caller) && !team.HasMember(caller) { + panic("only members / team address can perform commands on the team") + } + + // Get tasks for the given cmds + tasks := team.getTasksForCmds(caller, cmds...) + // Perform tasks + team.performTasks(caller, tasks...) +} + +// Default Access Controller + +func (team *Team) CanRun(member std.Address, cmd Cmd) bool { + if !team.IsRegister() { // team havn't been registered + return false + } + + isTeamAddress := team.IsTeamAddress(member) + isMember := team.HasMember(member) + + if !isMember && !isTeamAddress { + return false + } + + // TeamAddress has all the rights, until it has been burned. + if !team.burned && isTeamAddress { + return true + } + + // If access controller has been set, check access using `CanRun`. + if team.AccessController != nil { + return team.AccessController.CanRun(member, cmd) + } + + // Fallback on Default Permission + switch cmd.(type) { + case AddMemberCmd, AddPackageCmd: + return true // any member can do it + default: + } + + return false +} + +// Default Lifecycle implementation + +func (team *Team) Init() Task { + if team.Lifecycle != nil { + return team.Lifecycle.Init() + } + + return nil +} + +func (team *Team) ApplyUpdate(cmd Cmd) Task { + if team.Lifecycle != nil { + return team.Lifecycle.ApplyUpdate(cmd) + } + + return ApplyDefault(cmd) +} + +func ApplyDefault(cmd Cmd) Task { + switch typ := cmd.(type) { + case assertDefaultUpdateCmd: + typ.assert = true + return CreateTaskCmd(typ) // send it back + case AddMemberCmd: + return AddMemberTask(typ.Member) + case RemoveMemberCmd: + return RemoveMemberTask(typ.Member) + case AddPackageCmd: // XXX: + return nil + case UpdateAccessControllerCmd: + // Default UpdateAccessController can only update an empty + // AccessController + return CreateTask(func(t *Team) Cmd { + if t.AccessController != nil { + panic(`AccessController already set`) + } + + t.AccessController = typ.AccessController + return nil + }) + case UpdateLifecycleCmd: + // Default UpdateLifecycle can only update an empty + // Lifecycle + return CreateTask(func(t *Team) Cmd { + if t.Lifecycle != nil { + panic(`lifecycle already set`) + } + + t.Lifecycle = typ.Lifecycle + return nil + }) + } + + panic("command not supported: [" + cmd.Name() + `]`) +} + +func (team *Team) HasMember(member std.Address) bool { + return team.members.Has(member.String()) +} + +func (team *Team) IsTeamAddress(teamAddr std.Address) bool { + return teamAddr == team.address +} + +func (team *Team) IsRegister() bool { + return team.address != "" +} + +func (team *Team) Address() std.Address { + return team.address +} + +// Shortcut command + +func (team *Team) CanAddPackage(member std.Address) bool { + return team.CanRun(member, AddPackageCmd{}) +} + +// BurnTeamAddress prevent the team address from managing the team, leaving it +// to the members. +// BurnTeamAddress is a special command, and cannot be handle by team implementation. +// WARNING: This is irreversible +func (team *Team) BurnTeamAddress() { + team.Run(BurnTeamAddressCmd{}) +} + +func (team *Team) AddMember(member std.Address) { + team.Run(AddMemberCmd{Member: member}) +} + +func (team *Team) CanAddMember(member, target std.Address) bool { + return team.CanRun(member, AddMemberCmd{Member: member}) +} + +func (team *Team) RemoveMember(member std.Address) { + team.Run(RemoveMemberCmd{Member: member}) +} + +func (team *Team) CanRemoveMember(member, target std.Address) bool { + return team.CanRun(member, RemoveMemberCmd{Member: target}) +} + +func (team *Team) performTasks(caller std.Address, tasks ...Task) { + if team.isUpdating { + panic(`cannot perform task while updating, ensure returning the task instead`) + } + + var task Task + for len(tasks) > 0 { + task, tasks = tasks[0], tasks[1:] // Shift task + if task == nil { + continue // Skip empty task + } + + if cmd := task.call(team); cmd != nil { + nextTasks := team.getTasksForCmds(caller, cmd) + tasks = append(nextTasks, tasks...) + } + } +} + +func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { + team.isUpdating = true + defer func() { team.isUpdating = false }() + + tasks := make([]Task, 0, len(cmds)) + for _, cmd := range cmds { + if cmd == nil { + continue // Skip empty cmd + } + + switch typ := cmd.(type) { + case Cmds: + subTasks := team.getTasksForCmds(caller, typ...) + tasks = append(tasks, subTasks...) + case BurnTeamAddressCmd: // special case, can't be ignored + if team.burned { + panic("already burned") + } + + if !team.IsTeamAddress(caller) { + panic("only team address can burn") + } + + tasks = append(tasks, CreateTask(func(t *Team) Cmd { + team.burned = true + return nil + })) + + // XXX: do we fallthrough here? can be usefull for + // people to take some action but can also let people + // prevent burning by using `panic` + default: + // Assert caller can perform task + if !team.CanRun(caller, cmd) { + panic("unauthorized command for caller: [" + cmd.Name() + "]") + } + + // Prepare task + tasks = append(tasks, team.ApplyUpdate(cmd)) + } + } + return tasks +} + +type assertDefaultUpdateCmd struct{ assert bool } + +func (assertDefaultUpdateCmd) Name() string { return "assertDefaultUpdateCmd" } + +func assertDefaultUpdate(team *Team) { + task := team.ApplyUpdate(assertDefaultUpdateCmd{}) + cmd := task.call(team) + if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok { + if assertCmd.assert { + return + } + } + + panic("ensure that team implementation handles team update fallback") +} diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index cfbc85ffa96..63ebbe07b71 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -24,10 +24,7 @@ type Lifecycle interface { // // The Init method is called during the creation of a team to perform // initial setup actions such as adding founding members or configuring - // initial settings. Notably, this method operates with elevated privileges, - // meaning it does not enforce access control checks via the AccessController. - // This allows the initial setup to bypass usual restrictions, enabling - // foundational configuration without member-specific constraints. + // initial settings. // // Example: // func (m *MyTeam) Init() teams.Task { @@ -43,10 +40,11 @@ type Lifecycle interface { // // The ApplyUpdate method is used to modify the team's state in response // to commands such as adding or removing members, or changing member roles. - // Unlike Init, this method enforces access control checks using the - // AccessController. Before executing a command, ApplyUpdate ensures that - // the member issuing the command has the necessary permissions, maintaining - // the security and integrity of team operations. + // Before executing a command, ApplyUpdate ensures that the member + // issuing the command has the necessary permissions, maintaining the + // security and integrity of team operations. + // + // ApplyUpdate should always fallback on `teams.ApplyDefault`. // // Example: // func (m *MyTeam) ApplyUpdate(cmd teams.Cmd) teams.Task { @@ -72,18 +70,6 @@ type ITeam interface { var teams avl.Tree // std.Address -> *Team -type Team struct { - AccessController - Lifecycle - - burned bool - address std.Address // Origin address of the team - members avl.Tree // std.Address -> void - - // internal - isUpdating bool -} - // Realm of the form `xxx.xx/r//home` var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) @@ -112,7 +98,6 @@ func Register(iteam ITeam) *Team { Lifecycle: iteam, AccessController: iteam, } - // Assert that team implementation correctly uses fallback // XXX: do we want this? // It asserts that caller has a minimal implementation of its team @@ -127,207 +112,14 @@ func Register(iteam ITeam) *Team { return team } -// Run executes a series of commands on the team, ensuring that only authorized -// members can perform these operations. It translates commands into tasks and -// executes them to update the team's state. -// -// This is the entrypoint to execute command on the team. -// -// Flow: -// -// 1. **Caller Verification**: The method retrieves the original caller's address -// and checks if they are a member of the team. If not, it panics, preventing -// unauthorized access. -// -// 2. **Command Translation**: The method translates the provided commands into -// tasks. This involves checking if each command is authorized for execution -// by the caller and preparing the corresponding tasks. -// -// 3. **Task Execution**: The method executes the tasks, ensuring that each -// task is executed in sequence, effectively updating the team's state. -func (team *Team) Run(cmds ...Cmd) { - if !team.IsRegister() { - panic("team is not register") - } - - caller := std.GetOrigCaller() - if !team.IsTeamAddress(caller) && !team.HasMember(caller) { - panic("only members / team address can perform commands on the team") - } - - // Get tasks for the given cmds - tasks := team.getTasksForCmds(caller, cmds...) - // Perform tasks - team.performTasks(caller, tasks...) -} - -// Default Access Controller - -func (team *Team) CanRun(member std.Address, cmd Cmd) bool { - if !team.burned && team.IsTeamAddress(member) { - return true - } - - return team.AccessController != nil && team.AccessController.CanRun(member, cmd) -} - -// Default Lifecycle implementation - -func (team *Team) Init() Task { - if team.Lifecycle != nil { - return team.Lifecycle.Init() +func Get(teamAddr std.Address) *Team { + if t, ok := teams.Get(teamAddr.String()); ok { + return t.(*Team) } return nil } -func (team *Team) ApplyUpdate(cmd Cmd) Task { - if team.Lifecycle != nil { - return team.Lifecycle.ApplyUpdate(cmd) - } - - return ApplyDefault(cmd) -} - -func (team *Team) CanAddPackage(member std.Address, path string) bool { - return team.CanRun(member, AddPackageCmd{Path: path}) -} - -// BurnTeamAddress prevent the team address from managing the team, leaving it to the members. -// WARNING: This is irreversible -func (team *Team) BurnTeamAddress() { - team.Run(BurnTeamAddressCmd{}) -} - -func (team *Team) AddMember(member std.Address) { - team.Run(AddMemberCmd{Member: member}) -} - -func (team *Team) CanAddMember(member, target std.Address) bool { - return team.CanRun(member, AddMemberCmd{Member: member}) -} - -func (team *Team) RemoveMember(member std.Address) { - team.Run(RemoveMemberCmd{Member: member}) -} - -func (team *Team) CanRemoveMember(member, target std.Address) bool { - return team.CanRun(member, RemoveMemberCmd{Member: target}) -} - -func (team *Team) HasMember(member std.Address) bool { - return team.members.Has(member.String()) -} - -func (team *Team) IsTeamAddress(teamAddr std.Address) bool { - return teamAddr == team.address -} - -func (team *Team) IsRegister() bool { - return team.address != "" -} - -func (team *Team) Address() std.Address { - return team.address -} - -func ApplyDefault(cmd Cmd) Task { - switch typ := cmd.(type) { - case assertDefaultUpdateCmd: - typ.assert = true - return CreateTaskCmd(typ) - case AddMemberCmd: - return AddMemberTask(typ.Member) - case RemoveMemberCmd: - return RemoveMemberTask(typ.Member) - case AddPackageCmd: // XXX: - return nil - case UpdateAccessControllerCmd: - // this commands are not supported by default - } - - panic("command not supported") -} - func IsRegister(teamAddr std.Address) bool { return teams.Has(teamAddr.String()) } - -func (team *Team) performTasks(caller std.Address, tasks ...Task) { - if team.isUpdating { - panic(`cannot perform task while updating, ensure returning the task instead`) - } - - var task Task - for len(tasks) > 0 { - task, tasks = tasks[0], tasks[1:] // Shift task - if task == nil { - continue // Skip empty task - } - - if cmd := task.call(team); cmd != nil { - nextTasks := team.getTasksForCmds(caller, cmd) - tasks = append(nextTasks, tasks...) - } - } -} - -func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { - team.isUpdating = true - defer func() { team.isUpdating = false }() - - tasks := make([]Task, 0, len(cmds)) - for _, cmd := range cmds { - if cmd == nil { - continue // Skip empty cmd - } - - switch typ := cmd.(type) { - case Cmds: - subTasks := team.getTasksForCmds(caller, typ...) - tasks = append(tasks, subTasks...) - case BurnTeamAddressCmd: // special case, can't be ignored - if team.burned { - panic("already burned") - } - - if !team.IsTeamAddress(caller) { - panic("only team address can burn") - } - - tasks = append(tasks, CreateTask(func(t *Team) Cmd { - team.burned = true - return nil - })) - - // XXX: do we fallthrough here? can be usefull for - // people to take some action but can also let people - // prevent burning by using `panic` - default: - // Assert caller can perform task - if !team.CanRun(caller, cmd) { - panic("unauthorized command: " + "[" + cmd.Name() + "]") - } - - // Prepare task - tasks = append(tasks, team.ApplyUpdate(cmd)) - } - } - return tasks -} - -type assertDefaultUpdateCmd struct{ assert bool } - -func (assertDefaultUpdateCmd) Name() string { return "assertDefaultUpdateCmd" } - -func assertDefaultUpdate(team *Team) { - task := team.ApplyUpdate(assertDefaultUpdateCmd{}) - cmd := task.call(team) - if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok { - if assertCmd.assert { - return - } - } - - panic("ensure that team implementation handles team update fallback") -} diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno index 7b024f2411c..7cde483f6aa 100644 --- a/examples/gno.land/r/sys/teams/teams_ownable.gno +++ b/examples/gno.land/r/sys/teams/teams_ownable.gno @@ -41,6 +41,7 @@ func NewOwnableAccessController(ownable *ownable.Ownable) *OwnableAccessControll } func (o *OwnableAccessController) CanRun(member std.Address, cmd Cmd) bool { + if o.Ownable.Owner() == member { // All mighty owner return true } diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index 705423e3cca..d2ca4fe9b16 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -22,7 +22,7 @@ func main() { // Setup team user address println("team address is team address:", myteam.IsTeamAddress(teamAddress)) println("team address is not member:", !myteam.HasMember(teamAddress)) - println("team address can add package:", myteam.CanAddPackage(teamAddress, "")) + println("team address can add package:", myteam.CanAddPackage(teamAddress)) println("team address can add member alice:", myteam.CanAddMember(teamAddress, alice)) println("alice is not member:", !myteam.HasMember(alice)) diff --git a/examples/gno.land/r/sys/teams/z_9_filetest.gno b/examples/gno.land/r/sys/teams/z_9_filetest.gno index 75c41c1e0d2..919750d6d2f 100644 --- a/examples/gno.land/r/sys/teams/z_9_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_9_filetest.gno @@ -118,29 +118,29 @@ func init() { } func main() { + println(" -> register myteam") + myteamUser := myteam.Address() - println(myteamUser) // Setup team user address - println(" -> register myteam") - + println("myteamUser is team address:", myteam.IsTeamAddress(myteamUser)) println("myteamUser is not member:", !myteam.HasMember(myteamUser)) println("myteamUser has not level:", myteam.GetLevel(myteamUser) == LevelUnknown) - println("myteamUser can add package:", myteam.CanAddPackage(myteamUser, "")) + println("myteamUser can add package:", myteam.CanAddPackage(myteamUser)) // Setup test users alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") - println("alice cannot add a package:", !myteam.CanAddPackage(alice, "")) + println("alice cannot add a package:", !myteam.CanAddPackage(alice)) // Register alice to the team println(" -> adding alice as a member") myteam.AddMember(alice) println("alice is member:", myteam.HasMember(alice)) println("alice is level_1:", myteam.GetLevel(alice) == Level1) - println("alice can add package:", myteam.CanAddPackage(alice, "")) - println("bob cannot add package:", !myteam.CanAddPackage(bob, "")) + println("alice can add package:", myteam.CanAddPackage(alice)) + println("bob cannot add package:", !myteam.CanAddPackage(bob)) // Alice should not be able to add a member on level1 println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) @@ -153,9 +153,9 @@ func main() { println(" -> burn team address") myteam.BurnTeamAddress() - println("myteamUser is not member:", myteam.HasMember(myteamUser)) + println("myteamUser is not member:", !myteam.HasMember(myteamUser)) println("myteamUser is level_Unknown:", myteam.GetLevel(myteamUser) == LevelUnknown) - println("myteamUser cannot add package:", myteam.CanAddPackage(myteamUser, "")) + println("myteamUser cannot add package:", !myteam.CanAddPackage(myteamUser)) println("alice is still level_4:", myteam.GetLevel(alice) == Level4) println("alice can still add bob as member:", myteam.CanAddMember(alice, bob)) @@ -170,10 +170,40 @@ func main() { println("bob is member:", myteam.HasMember(bob)) println("bob is level_1:", myteam.GetLevel(bob) == Level1) - println("bob can add package:", myteam.CanAddPackage(bob, "")) + println("bob can add package:", myteam.CanAddPackage(bob)) println(" -> removing bob") myteam.RemoveMember(bob) println("bob is not member:", !myteam.HasMember(bob)) } + +// Output: +// -> register myteam +// myteamUser is team address: true +// myteamUser is not member: true +// myteamUser has not level: true +// myteamUser can add package: true +// alice cannot add a package: true +// -> adding alice as a member +// alice is member: true +// alice is level_1: true +// alice can add package: true +// bob cannot add package: true +// as level_1, alice cannot add bob as member: true +// -> setting alice to level 4 +// alice is level_4: true +// alice can add bob as member: true +// -> burn team address +// myteamUser is not member: true +// myteamUser is level_Unknown: true +// myteamUser cannot add package: true +// alice is still level_4: true +// alice can still add bob as member: true +// -> setting alice as origin caller +// -> alice add bob as member +// bob is member: true +// bob is level_1: true +// bob can add package: true +// -> removing bob +// bob is not member: true diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno index 71869fda1a1..ac156a8781e 100644 --- a/examples/gno.land/r/sys/users/verify.gno +++ b/examples/gno.land/r/sys/users/verify.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/r/demo/users" + "gno.land/r/sys/teams" ) const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul @@ -38,6 +39,12 @@ func VerifyNameByUser(enable bool, address std.Address, name string) bool { } if user := users.GetUserByName(name); user != nil { + // Check if team exist first + // XXX: for now a team is still user + if team := teams.Get(user.Address); team != nil { + return team.CanAddPackage(address) + } + return user.Address == address } diff --git a/gno.land/pkg/integration/testdata/register_team.txtar b/gno.land/pkg/integration/testdata/register_team.txtar new file mode 100644 index 00000000000..b4402e01ca0 --- /dev/null +++ b/gno.land/pkg/integration/testdata/register_team.txtar @@ -0,0 +1,98 @@ +# this testscript reproduce team register flow on https://github.com/gnolang/gno/issues/2195#issuecomment-2364056001 + +loadpkg gno.land/r/sys/teams +loadpkg gno.land/r/sys/users +loadpkg gno.land/r/demo/users + +adduser admin + +adduser alice +adduser bob + +patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # use our custom admin + +gnoland start + +# enable sys/users +gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 1000000 -broadcast -chainid tendermint_test admin +stdout 'OK!' + +# Try to add a pkg an with unregistered user +# alice addpkg -> gno.land/r/alice/foo +! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/foo -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test alice +stderr 'unauthorized user' + +# Test admin invites alice +# admin call -> demo/users.Invite +gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $alice_user_addr admin +stdout 'OK!' + +# Alice register alice namespace +# alice call -> demo/users.Register +gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $admin_user_addr -args 'alice' -args 'im alice' alice +stdout 'OK!' + +# Alice try to add a pkg on alice namespace +# alice addpkg -> gno.land/r/alice/foo +gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/foo -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test alice +stdout 'OK!' + +# Bob try to add a pkg on alice namespace +# bob addpkg -> gno.land/r/alice/bar +! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bar -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob +stderr 'unauthorized user' + +# Alice try register a team on a random namespace, should fail +# alice addpkg -> gno.land/r/alice/noop +! gnokey maketx addpkg -pkgdir $WORK/home -pkgpath gno.land/r/alice/noop -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test alice +stderr 'cannot register a team outside a home realm' + +# Alice try register a team on `home` namespace +# alice addpkg -> gno.land/r/alice/home +gnokey maketx addpkg -pkgdir $WORK/home -pkgpath gno.land/r/alice/home -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test alice +stdout 'OK!' + +# Bob try to add a pkg on alice namespace again +# bob addpkg -> gno.land/r/alice/bar +! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bar -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob +stderr 'unauthorized user' + +# Bob try to add himself as member +# bob call -> alice/home.AddMember(bob) +! gnokey maketx call -pkgpath gno.land/r/alice/home -func AddMember -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $bob_user_addr bob +stderr 'only members / team address can perform commands on the team' + +# Alice add bob as member +# alice call -> alice/home.AddMember(bob) +gnokey maketx call -pkgpath gno.land/r/alice/home -func AddMember -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $bob_user_addr alice +stdout 'OK!' + +# Bob add a pkg on alice namespace again, success ! +# bob addpkg -> gno.land/r/alice/bar +gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bar -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob +stdout 'OK!' + +-- home/myteam.gno -- +package home + +import ( + "std" + "gno.land/r/sys/teams" +) + +var myteam *teams.Team + +func AddMember(addr std.Address) { + myteam.AddMember(addr) +} + +func init() { + myteam = teams.Register(nil) +} + +-- mypkg/mypkg.gno -- +package mypkg + +func Render(path string) string { + return "# Hello Mypkg" +} From 746128d2480563af75c937c215fbe0acb082a965 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:43:58 +0100 Subject: [PATCH 07/18] chore: cleanup comments Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/cmd.gno | 2 +- examples/gno.land/r/sys/teams/team.gno | 106 +++++++----------- examples/gno.land/r/sys/teams/teams.gno | 21 ++-- .../gno.land/r/sys/teams/teams_ownable.gno | 6 +- examples/gno.land/r/sys/teams/teams_test.gno | 2 + .../integration/testdata/register_team.txtar | 2 +- 6 files changed, 62 insertions(+), 77 deletions(-) diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno index b3bd878555b..03b86b2b8e8 100644 --- a/examples/gno.land/r/sys/teams/cmd.gno +++ b/examples/gno.land/r/sys/teams/cmd.gno @@ -6,11 +6,11 @@ import ( "strings" ) +// XXX: Improve errors var ErrAlreadyExist = errors.New("already exists") var ErrDoesNotExist = errors.New("does not exist") // Cmd represents a command that can be executed on a team. -// Each command should implement the Name method to provide a string identifier. type Cmd interface { Name() string } diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno index cfb5c912870..17d7ee79b72 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/team.gno @@ -10,40 +10,28 @@ type Team struct { AccessController Lifecycle - address std.Address // Origin address of the team - members avl.Tree // std.Address -> void - burned bool - - // internal - isUpdating bool + address std.Address // Origin address of the team + members avl.Tree // std.Address -> void + burned bool + isUpdating bool // internal flag for update status } // Run executes a series of commands on the team, ensuring that only authorized // members can perform these operations. It translates commands into tasks and // executes them to update the team's state. // -// This is the entrypoint to execute command on the team. -// // Flow: -// -// 1. **Caller Verification**: The method retrieves the original caller's address -// and checks if they are a member of the team. If not, it panics, preventing -// unauthorized access. -// -// 2. **Command Translation**: The method translates the provided commands into -// tasks. This involves checking if each command is authorized for execution -// by the caller and preparing the corresponding tasks. -// -// 3. **Task Execution**: The method executes the tasks, ensuring that each -// task is executed in sequence, effectively updating the team's state. +// 1. **Caller Verification**: Checks if the caller is a team member or the team address. +// 2. **Command Translation**: Translates commands into tasks for execution. +// 3. **Task Execution**: Executes tasks in sequence, updating the team's state. func (team *Team) Run(cmds ...Cmd) { - if !team.IsRegister() { - panic("team is not register") + if !team.IsRegistered() { + panic("team is not registered") } caller := std.GetOrigCaller() if !team.IsTeamAddress(caller) && !team.HasMember(caller) { - panic("only members / team address can perform commands on the team") + panic("only members or team address can perform commands on the team") } // Get tasks for the given cmds @@ -52,10 +40,9 @@ func (team *Team) Run(cmds ...Cmd) { team.performTasks(caller, tasks...) } -// Default Access Controller - +// CanRun checks if a member can run a specific command. func (team *Team) CanRun(member std.Address, cmd Cmd) bool { - if !team.IsRegister() { // team havn't been registered + if !team.IsRegistered() { // team hasn't been registered return false } @@ -66,12 +53,12 @@ func (team *Team) CanRun(member std.Address, cmd Cmd) bool { return false } - // TeamAddress has all the rights, until it has been burned. + // TeamAddress has all the rights until it has been burned. if !team.burned && isTeamAddress { return true } - // If access controller has been set, check access using `CanRun`. + // If an AccessController is set, delegate the check. if team.AccessController != nil { return team.AccessController.CanRun(member, cmd) } @@ -81,29 +68,27 @@ func (team *Team) CanRun(member std.Address, cmd Cmd) bool { case AddMemberCmd, AddPackageCmd: return true // any member can do it default: + return false } - - return false } -// Default Lifecycle implementation - +// Init initializes the team lifecycle. func (team *Team) Init() Task { if team.Lifecycle != nil { return team.Lifecycle.Init() } - return nil } +// ApplyUpdate applies a command update to the team. func (team *Team) ApplyUpdate(cmd Cmd) Task { if team.Lifecycle != nil { return team.Lifecycle.ApplyUpdate(cmd) } - return ApplyDefault(cmd) } +// ApplyDefault handles default command updates. func ApplyDefault(cmd Cmd) Task { switch typ := cmd.(type) { case assertDefaultUpdateCmd: @@ -113,84 +98,84 @@ func ApplyDefault(cmd Cmd) Task { return AddMemberTask(typ.Member) case RemoveMemberCmd: return RemoveMemberTask(typ.Member) - case AddPackageCmd: // XXX: + case AddPackageCmd: // XXX: Consider implementation return nil case UpdateAccessControllerCmd: - // Default UpdateAccessController can only update an empty - // AccessController return CreateTask(func(t *Team) Cmd { if t.AccessController != nil { - panic(`AccessController already set`) + panic("AccessController already set") } - t.AccessController = typ.AccessController return nil }) case UpdateLifecycleCmd: - // Default UpdateLifecycle can only update an empty - // Lifecycle return CreateTask(func(t *Team) Cmd { if t.Lifecycle != nil { - panic(`lifecycle already set`) + panic("lifecycle already set") } - t.Lifecycle = typ.Lifecycle return nil }) + default: + panic("command not supported: [" + cmd.Name() + "]") } - - panic("command not supported: [" + cmd.Name() + `]`) } +// HasMember checks if a given address is a member of the team. func (team *Team) HasMember(member std.Address) bool { return team.members.Has(member.String()) } +// IsTeamAddress checks if a given address is the team's address. func (team *Team) IsTeamAddress(teamAddr std.Address) bool { return teamAddr == team.address } -func (team *Team) IsRegister() bool { +// IsRegistered checks if the team is registered. +func (team *Team) IsRegistered() bool { return team.address != "" } +// Address returns the team's address. func (team *Team) Address() std.Address { return team.address } -// Shortcut command - +// CanAddPackage checks if a member can add a package. func (team *Team) CanAddPackage(member std.Address) bool { return team.CanRun(member, AddPackageCmd{}) } -// BurnTeamAddress prevent the team address from managing the team, leaving it -// to the members. -// BurnTeamAddress is a special command, and cannot be handle by team implementation. -// WARNING: This is irreversible +// BurnTeamAddress prevents the team address from managing the team, leaving it to the members. +// WARNING: This is irreversible. func (team *Team) BurnTeamAddress() { team.Run(BurnTeamAddressCmd{}) } +// AddMember adds a member to the team. func (team *Team) AddMember(member std.Address) { team.Run(AddMemberCmd{Member: member}) } +// CanAddMember checks if a member can add another member. func (team *Team) CanAddMember(member, target std.Address) bool { return team.CanRun(member, AddMemberCmd{Member: member}) } +// RemoveMember removes a member from the team. func (team *Team) RemoveMember(member std.Address) { team.Run(RemoveMemberCmd{Member: member}) } +// CanRemoveMember checks if a member can remove another member. func (team *Team) CanRemoveMember(member, target std.Address) bool { return team.CanRun(member, RemoveMemberCmd{Member: target}) } +// performTasks executes a list of tasks. func (team *Team) performTasks(caller std.Address, tasks ...Task) { if team.isUpdating { - panic(`cannot perform task while updating, ensure returning the task instead`) + panic("cannot perform task while updating, ensure returning the task instead") } var task Task @@ -207,6 +192,7 @@ func (team *Team) performTasks(caller std.Address, tasks ...Task) { } } +// getTasksForCmds translates commands into tasks. func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { team.isUpdating = true defer func() { team.isUpdating = false }() @@ -221,7 +207,7 @@ func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { case Cmds: subTasks := team.getTasksForCmds(caller, typ...) tasks = append(tasks, subTasks...) - case BurnTeamAddressCmd: // special case, can't be ignored + case BurnTeamAddressCmd: if team.burned { panic("already burned") } @@ -235,16 +221,12 @@ func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { return nil })) - // XXX: do we fallthrough here? can be usefull for - // people to take some action but can also let people - // prevent burning by using `panic` + // XXX: Consider if fallthrough is needed default: - // Assert caller can perform task if !team.CanRun(caller, cmd) { panic("unauthorized command for caller: [" + cmd.Name() + "]") } - // Prepare task tasks = append(tasks, team.ApplyUpdate(cmd)) } } @@ -255,14 +237,12 @@ type assertDefaultUpdateCmd struct{ assert bool } func (assertDefaultUpdateCmd) Name() string { return "assertDefaultUpdateCmd" } +// assertDefaultUpdate ensures the team implementation handles team update fallback. func assertDefaultUpdate(team *Team) { task := team.ApplyUpdate(assertDefaultUpdateCmd{}) cmd := task.call(team) - if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok { - if assertCmd.assert { - return - } + if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok && assertCmd.assert { + return } - panic("ensure that team implementation handles team update fallback") } diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index 63ebbe07b71..ec44574b0fb 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -29,8 +29,9 @@ type Lifecycle interface { // Example: // func (m *MyTeam) Init() teams.Task { // return teams.CreateTaskCmd( - // teams.AddMemberCmd{myteam.MyUser}, // Add MyUser as a member - // SetLevelCmd{myteam.MyUser, Level4}, // Set MyUser's level to Level4 + // AddMemberTask(m.Owner()), // Add Owner as Member + // SetLevelTask(m.Owner(), Level4), // Set Owner's level to Level4 + // BurnTeamAddressTask, // Make team address unusable // ) // } Init() Task @@ -68,23 +69,24 @@ type ITeam interface { Lifecycle } -var teams avl.Tree // std.Address -> *Team +var teams avl.Tree // std.Address -> *Team -// Realm of the form `xxx.xx/r//home` +// reHomeRealm validate thehome realm path format `xxx.xx/r//home`. +// XXX: Use somthing simpler var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) // Register creates and registers a new team in the `r/sys/teams` realm, allowing a registered // user to transform into a team by publishing a contract that registers members in a registry. // // The `Register` function verifies that the caller is a valid home path (`r//home`), -// using `` as the team name, and ensures that the caller is not already registered as a team. +// using `` as the team name. func Register(iteam ITeam) *Team { caller := std.GetOrigCaller() realm := std.PrevRealm() // Check if caller is not already registered as a team if teams.Has(caller.String()) { - panic("team already registered: " + caller) + panic("team already registered: " + caller.String()) } // Check if origin caller is a home path @@ -92,7 +94,7 @@ func Register(iteam ITeam) *Team { panic("cannot register a team outside a home realm") } - // Then initilize the team + // Initialize the team team := &Team{ address: caller, Lifecycle: iteam, @@ -100,7 +102,7 @@ func Register(iteam ITeam) *Team { } // Assert that team implementation correctly uses fallback // XXX: do we want this? - // It asserts that caller has a minimal implementation of its team + // It asserts that caller has a minimal implementation of Update assertDefaultUpdate(team) if initTask := team.Init(); initTask != nil { @@ -112,14 +114,15 @@ func Register(iteam ITeam) *Team { return team } +// Get retrieves a registered team by address. func Get(teamAddr std.Address) *Team { if t, ok := teams.Get(teamAddr.String()); ok { return t.(*Team) } - return nil } +// IsRegister checks if a team is already registered by address. func IsRegister(teamAddr std.Address) bool { return teams.Has(teamAddr.String()) } diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno index 7cde483f6aa..9d42a9d18da 100644 --- a/examples/gno.land/r/sys/teams/teams_ownable.gno +++ b/examples/gno.land/r/sys/teams/teams_ownable.gno @@ -19,7 +19,7 @@ func NewOwnableTeam(owner *ownable.Ownable) ITeam { func (o *OwnableTeam) Init() Task { return ChainTasks( AddMemberTask(o.Owner()), - // burn team address, so only owner can control the team + // Burn team address, so only owner can control the team BurnTeamAddressTask, ) } @@ -33,7 +33,7 @@ type OwnableAccessController struct { EnableAddMember bool EnableRemoveMember bool - DisableAddPackage bool + EnableAddPackage bool } func NewOwnableAccessController(ownable *ownable.Ownable) *OwnableAccessController { @@ -52,7 +52,7 @@ func (o *OwnableAccessController) CanRun(member std.Address, cmd Cmd) bool { case RemoveMemberCmd: return o.EnableRemoveMember case AddPackageCmd: - return !o.DisableAddPackage + return o.EnableAddPackage } return false diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno index bd827e5e875..e403cc9bd87 100644 --- a/examples/gno.land/r/sys/teams/teams_test.gno +++ b/examples/gno.land/r/sys/teams/teams_test.gno @@ -1 +1,3 @@ package teams + +// XXX: TODO diff --git a/gno.land/pkg/integration/testdata/register_team.txtar b/gno.land/pkg/integration/testdata/register_team.txtar index b4402e01ca0..d1531369f7a 100644 --- a/gno.land/pkg/integration/testdata/register_team.txtar +++ b/gno.land/pkg/integration/testdata/register_team.txtar @@ -60,7 +60,7 @@ stderr 'unauthorized user' # Bob try to add himself as member # bob call -> alice/home.AddMember(bob) ! gnokey maketx call -pkgpath gno.land/r/alice/home -func AddMember -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $bob_user_addr bob -stderr 'only members / team address can perform commands on the team' +stderr 'only members or team address can perform commands on the team' # Alice add bob as member # alice call -> alice/home.AddMember(bob) From eefbae94df2bec277ddac8422fcbbbcc0498fab2 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:03:59 +0100 Subject: [PATCH 08/18] chore: cleanup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/z_1_filetest.gno | 12 +++++++++++- examples/gno.land/r/sys/teams/z_9_filetest.gno | 10 ++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index d2ca4fe9b16..0a7e75b2c10 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -24,9 +24,19 @@ func main() { println("team address is not member:", !myteam.HasMember(teamAddress)) println("team address can add package:", myteam.CanAddPackage(teamAddress)) println("team address can add member alice:", myteam.CanAddMember(teamAddress, alice)) - println("alice is not member:", !myteam.HasMember(alice)) + println(" -> adding alice as member") myteam.AddMember(alice) + println("alice is now a member:", myteam.HasMember(alice)) } + +// Output: +// team address is team address: true +// team address is not member: true +// team address can add package: true +// team address can add member alice: true +// alice is not member: true +// -> adding alice as member +// alice is now a member: true diff --git a/examples/gno.land/r/sys/teams/z_9_filetest.gno b/examples/gno.land/r/sys/teams/z_9_filetest.gno index 919750d6d2f..a85353177d5 100644 --- a/examples/gno.land/r/sys/teams/z_9_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_9_filetest.gno @@ -118,8 +118,7 @@ func init() { } func main() { - println(" -> register myteam") - + println("* registered myteam") myteamUser := myteam.Address() // Setup team user address @@ -137,22 +136,25 @@ func main() { // Register alice to the team println(" -> adding alice as a member") myteam.AddMember(alice) + println("alice is member:", myteam.HasMember(alice)) println("alice is level_1:", myteam.GetLevel(alice) == Level1) println("alice can add package:", myteam.CanAddPackage(alice)) println("bob cannot add package:", !myteam.CanAddPackage(bob)) - // Alice should not be able to add a member on level1 println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) // Update alice to Level4 println(" -> setting alice to level 4") myteam.SetLevel(alice, Level4) + println("alice is level_4:", myteam.GetLevel(alice) == Level4) println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) + // Burn team address println(" -> burn team address") myteam.BurnTeamAddress() + println("myteamUser is not member:", !myteam.HasMember(myteamUser)) println("myteamUser is level_Unknown:", myteam.GetLevel(myteamUser) == LevelUnknown) println("myteamUser cannot add package:", !myteam.CanAddPackage(myteamUser)) @@ -179,7 +181,7 @@ func main() { } // Output: -// -> register myteam +// * registered myteam // myteamUser is team address: true // myteamUser is not member: true // myteamUser has not level: true From daaaf4e0bc2707040cfcd2160e5cb50e7606f803 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:31:45 +0100 Subject: [PATCH 09/18] chore: cleanup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/pkg/gnolang/values.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index aff65ecda48..da887764c8e 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1963,7 +1963,7 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val "native type %s has no method or field %s", dtv.T.String(), path.Name)) default: - panic("should not happen: " + fmt.Sprintf("path: %#v", path)) + panic("should not happen") } } From 9fa08a3f6cfb08b99783c2381748f3062cfef877 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:06:05 +0100 Subject: [PATCH 10/18] feat: teams v2 Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/cmd.gno | 119 -------- examples/gno.land/r/sys/teams/gno.mod | 2 +- examples/gno.land/r/sys/teams/task.gno | 85 ------ examples/gno.land/r/sys/teams/team.gno | 268 ++++-------------- examples/gno.land/r/sys/teams/teams.gno | 128 --------- .../gno.land/r/sys/teams/teams_ownable.gno | 59 ---- examples/gno.land/r/sys/teams/teams_test.gno | 3 - .../gno.land/r/sys/teams/z_1_filetest.gno | 42 --- .../gno.land/r/sys/teams/z_9_filetest.gno | 211 -------------- 9 files changed, 51 insertions(+), 866 deletions(-) delete mode 100644 examples/gno.land/r/sys/teams/cmd.gno delete mode 100644 examples/gno.land/r/sys/teams/task.gno delete mode 100644 examples/gno.land/r/sys/teams/teams.gno delete mode 100644 examples/gno.land/r/sys/teams/teams_ownable.gno delete mode 100644 examples/gno.land/r/sys/teams/teams_test.gno delete mode 100644 examples/gno.land/r/sys/teams/z_1_filetest.gno delete mode 100644 examples/gno.land/r/sys/teams/z_9_filetest.gno diff --git a/examples/gno.land/r/sys/teams/cmd.gno b/examples/gno.land/r/sys/teams/cmd.gno deleted file mode 100644 index 03b86b2b8e8..00000000000 --- a/examples/gno.land/r/sys/teams/cmd.gno +++ /dev/null @@ -1,119 +0,0 @@ -package teams - -import ( - "errors" - "std" - "strings" -) - -// XXX: Improve errors -var ErrAlreadyExist = errors.New("already exists") -var ErrDoesNotExist = errors.New("does not exist") - -// Cmd represents a command that can be executed on a team. -type Cmd interface { - Name() string -} - -type Cmds []Cmd - -func (cmds Cmds) Name() string { - var str strings.Builder - str.WriteRune('[') - for i, cmd := range cmds { - if i > 0 { - str.WriteRune(',') - } - - str.WriteString(cmd.Name()) - } - str.WriteRune(']') - return str.String() -} - -// AddMemberCmd represents a command to add a member to the team. -type AddMemberCmd struct { - Member std.Address -} - -func (cmd AddMemberCmd) Name() string { return "AddMember" } - -// AddMemberTask creates a task to add a member to the team. -func AddMemberTask(member std.Address) Task { - return CreateTask(func(t *Team) Cmd { - // Cannot add team address as a member - if t.IsTeamAddress(member) { - panic("cannot add team address as a member") - } - - if t.members.Has(member.String()) { - panic(ErrAlreadyExist) - } - t.members.Set(member.String(), struct{}{}) - return nil - }) -} - -// RemoveMemberCmd represents a command to remove a member from the team. -type RemoveMemberCmd struct { - Member std.Address -} - -func (cmd RemoveMemberCmd) Name() string { return "RemoveMember" } - -// RemoveMemberTask creates a task to remove a member from the team. -func RemoveMemberTask(member std.Address) Task { - return CreateTask(func(t *Team) Cmd { - if !t.members.Has(member.String()) { - panic(ErrDoesNotExist) - } - t.members.Remove(member.String()) - return nil - }) -} - -// The command bellow should be use with precaution - -// UpdateAccessControllerCmd represents a command to update the team's access controller. -type UpdateAccessControllerCmd struct { - AccessController -} - -func (cmd UpdateAccessControllerCmd) Name() string { return "UpdateAccessController" } - -// UpdateAccessControllerTask creates a task to update the team's access controller. -func UpdateAccessControllerTask(ac AccessController) Task { - return CreateTask(func(t *Team) Cmd { - t.AccessController = ac - return nil - }) -} - -// UpdateLifecycleCmd represents a command to update the team's access controller. -type UpdateLifecycleCmd struct { - Lifecycle -} - -func (cmd UpdateLifecycleCmd) Name() string { return "UpdateLifecycle" } - -// UpdateLifecycleTask creates a task to update the team's access controller. -func UpdateLifecycleTask(ac Lifecycle) Task { - return CreateTask(func(t *Team) Cmd { - t.Lifecycle = ac - return nil - }) -} - -// BurnTeamAddressCmd represents a command to burn the team's address. -type BurnTeamAddressCmd struct{} - -func (cmd BurnTeamAddressCmd) Name() string { return "BurnTeamAddress" } - -var BurnTeamAddressTask = CreateTaskCmd(BurnTeamAddressCmd{}) - -// AddPackageCmd represents a command to add a package to the team. -type AddPackageCmd struct { - Path string -} - -func (cmd AddPackageCmd) Name() string { return "AddPackage" } diff --git a/examples/gno.land/r/sys/teams/gno.mod b/examples/gno.land/r/sys/teams/gno.mod index 4afbd1a0fe9..1d7dc7853c2 100644 --- a/examples/gno.land/r/sys/teams/gno.mod +++ b/examples/gno.land/r/sys/teams/gno.mod @@ -1 +1 @@ -module gno.land/r/sys/teams \ No newline at end of file +module gno.land/r/sys/teams2 \ No newline at end of file diff --git a/examples/gno.land/r/sys/teams/task.gno b/examples/gno.land/r/sys/teams/task.gno deleted file mode 100644 index 1ea1020f798..00000000000 --- a/examples/gno.land/r/sys/teams/task.gno +++ /dev/null @@ -1,85 +0,0 @@ -package teams - -// TaskFunc defines a function type that takes a Team and returns a Cmd. -// It represents the executable logic that can be performed on a team. -type TaskFunc func(t *Team) Cmd - -// Task represents a unit of work that can be executed to apply a command. -// Tasks encapsulate the execution logic of commands, ensuring that operations -// are performed with the correct permissions and in a specified order. -// -// Tasks are used to execute commands that modify the team's state, ensuring -// that these modifications adhere to the permissions set by the access control -// mechanisms and are executed in a controlled sequence. -type Task interface { - call(t *Team) Cmd -} - -type task struct { - actionFunc TaskFunc -} - -func (a task) call(t *Team) Cmd { - return a.actionFunc(t) -} - -// CreateTaskCmd creates a Task from one or more commands. -func CreateTaskCmd(cmd ...Cmd) Task { - switch len(cmd) { - case 0: - return nil - case 1: - return CreateTask(func(_ *Team) Cmd { - return cmd[0] - }) - default: - // Handle multiple commands - } - - fns := make([]TaskFunc, len(cmd)) - for i, m := range cmd { - fns[i] = func(_ *Team) Cmd { - return m - } - } - return CreateTask(fns...) -} - -// CreateTask creates a Task from one or more TaskFuncs. -func CreateTask(fn ...TaskFunc) Task { - switch len(fn) { - case 0: - return nil - case 1: - return &task{actionFunc: fn[0]} - default: - // Handle multiple functions - } - - actions := make([]Task, len(fn)) - for i, f := range fn { - actions[i] = &task{actionFunc: f} - } - return ChainTasks(actions...) -} - -// ChainTasks creates a single Task that executes a series of tasks in sequence. -// It combines multiple tasks into one. -func ChainTasks(actions ...Task) Task { - switch len(actions) { - case 0: - return nil - case 1: - return actions[0] - default: - // Handle chaining of multiple tasks - } - - return CreateTask(func(t *Team) Cmd { - cmds := make([]Cmd, len(actions)) - for i, action := range actions { - cmds[i] = action.call(t) - } - return Cmds(cmds) - }) -} diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno index 17d7ee79b72..1fc41a9968c 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/team.gno @@ -1,248 +1,80 @@ +// Package team provides decentralized team management with permission control. +// Teams are identified by addresses and manage member permissions. package teams import ( + "errors" "std" + "strings" "gno.land/p/demo/avl" ) -type Team struct { - AccessController - Lifecycle +var teams avl.Tree // std.Address -> Team - address std.Address // Origin address of the team - members avl.Tree // std.Address -> void - burned bool - isUpdating bool // internal flag for update status -} - -// Run executes a series of commands on the team, ensuring that only authorized -// members can perform these operations. It translates commands into tasks and -// executes them to update the team's state. -// -// Flow: -// 1. **Caller Verification**: Checks if the caller is a team member or the team address. -// 2. **Command Translation**: Translates commands into tasks for execution. -// 3. **Task Execution**: Executes tasks in sequence, updating the team's state. -func (team *Team) Run(cmds ...Cmd) { - if !team.IsRegistered() { - panic("team is not registered") - } - - caller := std.GetOrigCaller() - if !team.IsTeamAddress(caller) && !team.HasMember(caller) { - panic("only members or team address can perform commands on the team") - } - - // Get tasks for the given cmds - tasks := team.getTasksForCmds(caller, cmds...) - // Perform tasks - team.performTasks(caller, tasks...) -} - -// CanRun checks if a member can run a specific command. -func (team *Team) CanRun(member std.Address, cmd Cmd) bool { - if !team.IsRegistered() { // team hasn't been registered - return false - } - - isTeamAddress := team.IsTeamAddress(member) - isMember := team.HasMember(member) - - if !isMember && !isTeamAddress { - return false - } +var ( + ErrNotHomeCaller = errors.New("must be called from r//home realm") + ErrUnauthorized = errors.New("unauthorized operation") + ErrInvalidTeam = errors.New("invalid or unknown team") +) - // TeamAddress has all the rights until it has been burned. - if !team.burned && isTeamAddress { - return true - } +type Action interface{} - // If an AccessController is set, delegate the check. - if team.AccessController != nil { - return team.AccessController.CanRun(member, cmd) - } +type ( + ActionAddPackage struct{ Path string } + // NOTE: More actions to come... +) - // Fallback on Default Permission - switch cmd.(type) { - case AddMemberCmd, AddPackageCmd: - return true // any member can do it - default: - return false - } -} +// Team defines the core capabilities for team management +type Team interface { + Can(member std.Address, action Action) bool -// Init initializes the team lifecycle. -func (team *Team) Init() Task { - if team.Lifecycle != nil { - return team.Lifecycle.Init() - } - return nil + // Members listing + Size() int + HasMember(member std.Address) bool + PageMember(pageNumber, pageSize int) []std.Address } -// ApplyUpdate applies a command update to the team. -func (team *Team) ApplyUpdate(cmd Cmd) Task { - if team.Lifecycle != nil { - return team.Lifecycle.ApplyUpdate(cmd) +// Register creates a new team from a home realm +func Register() { + caller := std.GetOrigCaller() + if teams.Has(caller.String()) { + panic("team already exists: " + caller.String()) } - return ApplyDefault(cmd) -} -// ApplyDefault handles default command updates. -func ApplyDefault(cmd Cmd) Task { - switch typ := cmd.(type) { - case assertDefaultUpdateCmd: - typ.assert = true - return CreateTaskCmd(typ) // send it back - case AddMemberCmd: - return AddMemberTask(typ.Member) - case RemoveMemberCmd: - return RemoveMemberTask(typ.Member) - case AddPackageCmd: // XXX: Consider implementation - return nil - case UpdateAccessControllerCmd: - return CreateTask(func(t *Team) Cmd { - if t.AccessController != nil { - panic("AccessController already set") - } - t.AccessController = typ.AccessController - return nil - }) - case UpdateLifecycleCmd: - return CreateTask(func(t *Team) Cmd { - if t.Lifecycle != nil { - panic("lifecycle already set") - } - t.Lifecycle = typ.Lifecycle - return nil - }) - default: - panic("command not supported: [" + cmd.Name() + "]") + prev := std.PrevRealm() + pathParts := strings.Split(prev.PkgPath(), "/") + if len(pathParts) != 4 || pathParts[2] != caller.String() || pathParts[3] != "home" { + panic(ErrNotHomeCaller) } -} - -// HasMember checks if a given address is a member of the team. -func (team *Team) HasMember(member std.Address) bool { - return team.members.Has(member.String()) -} - -// IsTeamAddress checks if a given address is the team's address. -func (team *Team) IsTeamAddress(teamAddr std.Address) bool { - return teamAddr == team.address -} - -// IsRegistered checks if the team is registered. -func (team *Team) IsRegistered() bool { - return team.address != "" -} - -// Address returns the team's address. -func (team *Team) Address() std.Address { - return team.address -} - -// CanAddPackage checks if a member can add a package. -func (team *Team) CanAddPackage(member std.Address) bool { - return team.CanRun(member, AddPackageCmd{}) -} - -// BurnTeamAddress prevents the team address from managing the team, leaving it to the members. -// WARNING: This is irreversible. -func (team *Team) BurnTeamAddress() { - team.Run(BurnTeamAddressCmd{}) -} -// AddMember adds a member to the team. -func (team *Team) AddMember(member std.Address) { - team.Run(AddMemberCmd{Member: member}) + teams.Set(caller.String(), &BasicTeam{}) } -// CanAddMember checks if a member can add another member. -func (team *Team) CanAddMember(member, target std.Address) bool { - return team.CanRun(member, AddMemberCmd{Member: member}) -} - -// RemoveMember removes a member from the team. -func (team *Team) RemoveMember(member std.Address) { - team.Run(RemoveMemberCmd{Member: member}) -} - -// CanRemoveMember checks if a member can remove another member. -func (team *Team) CanRemoveMember(member, target std.Address) bool { - return team.CanRun(member, RemoveMemberCmd{Member: target}) -} - -// performTasks executes a list of tasks. -func (team *Team) performTasks(caller std.Address, tasks ...Task) { - if team.isUpdating { - panic("cannot perform task while updating, ensure returning the task instead") +func Get(team std.Address) Team { + if val, ok := teams.Get(team.String()); ok { + return val.(Team) } - var task Task - for len(tasks) > 0 { - task, tasks = tasks[0], tasks[1:] // Shift task - if task == nil { - continue // Skip empty task - } - - if cmd := task.call(team); cmd != nil { - nextTasks := team.getTasksForCmds(caller, cmd) - tasks = append(nextTasks, tasks...) - } - } + return nil } -// getTasksForCmds translates commands into tasks. -func (team *Team) getTasksForCmds(caller std.Address, cmds ...Cmd) []Task { - team.isUpdating = true - defer func() { team.isUpdating = false }() - - tasks := make([]Task, 0, len(cmds)) - for _, cmd := range cmds { - if cmd == nil { - continue // Skip empty cmd - } - - switch typ := cmd.(type) { - case Cmds: - subTasks := team.getTasksForCmds(caller, typ...) - tasks = append(tasks, subTasks...) - case BurnTeamAddressCmd: - if team.burned { - panic("already burned") - } - - if !team.IsTeamAddress(caller) { - panic("only team address can burn") - } - - tasks = append(tasks, CreateTask(func(t *Team) Cmd { - team.burned = true - return nil - })) - - // XXX: Consider if fallthrough is needed - default: - if !team.CanRun(caller, cmd) { - panic("unauthorized command for caller: [" + cmd.Name() + "]") - } - - tasks = append(tasks, team.ApplyUpdate(cmd)) - } +// SetMember updates permissions for a team member +func HasMember(team, member std.Address) bool { + caller := std.GetOrigCaller() + val, exists := teams.Get(caller.String()) + if !exists { + panic(ErrInvalidTeam) } - return tasks -} -type assertDefaultUpdateCmd struct{ assert bool } - -func (assertDefaultUpdateCmd) Name() string { return "assertDefaultUpdateCmd" } + return val.(Team).HasMember(member) +} -// assertDefaultUpdate ensures the team implementation handles team update fallback. -func assertDefaultUpdate(team *Team) { - task := team.ApplyUpdate(assertDefaultUpdateCmd{}) - cmd := task.call(team) - if assertCmd, ok := cmd.(assertDefaultUpdateCmd); ok && assertCmd.assert { - return +// CanTeamMemberDo checks permissions across teams +func MemberCanDo(team, member std.Address, action string) bool { + val, exists := teams.Get(team.String()) + if !exists { + return false } - panic("ensure that team implementation handles team update fallback") + return val.(Team).Can(member, action) } diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno deleted file mode 100644 index ec44574b0fb..00000000000 --- a/examples/gno.land/r/sys/teams/teams.gno +++ /dev/null @@ -1,128 +0,0 @@ -package teams - -import ( - "regexp" - "std" - - "gno.land/p/demo/avl" -) - -// AccessController defines the interface for controlling access to team commands. -// Implementations should define the logic to determine if a member can run a specific command. -type AccessController interface { - // CanRun determines if a member is authorized to execute a specific command. - CanRun(member std.Address, cmd Cmd) bool -} - -// Lifecycle defines the interface for managing the lifecycle of a team. -// It provides methods for initializing a team and applying updates through commands. -// Implementations of this interface should define how a team is set up initially -// and how it responds to changes over time. -type Lifecycle interface { - // Init initializes the team with a series of commands. - // It returns a Task that encapsulates the initialization logic. - // - // The Init method is called during the creation of a team to perform - // initial setup actions such as adding founding members or configuring - // initial settings. - // - // Example: - // func (m *MyTeam) Init() teams.Task { - // return teams.CreateTaskCmd( - // AddMemberTask(m.Owner()), // Add Owner as Member - // SetLevelTask(m.Owner(), Level4), // Set Owner's level to Level4 - // BurnTeamAddressTask, // Make team address unusable - // ) - // } - Init() Task - - // ApplyUpdate applies an update to the team based on a given command. - // It returns a Task that represents the execution of this update. - // - // The ApplyUpdate method is used to modify the team's state in response - // to commands such as adding or removing members, or changing member roles. - // Before executing a command, ApplyUpdate ensures that the member - // issuing the command has the necessary permissions, maintaining the - // security and integrity of team operations. - // - // ApplyUpdate should always fallback on `teams.ApplyDefault`. - // - // Example: - // func (m *MyTeam) ApplyUpdate(cmd teams.Cmd) teams.Task { - // switch typ := cmd.(type) { - // case SetLevelCmd: - // return teams.CreateTask(func(_ *teams.Team) teams.Cmd { - // m.levels.Set(typ.Member.String(), typ.Level) - // return nil - // }) - // } - // return team.ApplyDefault(cmd) - // } - ApplyUpdate(cmd Cmd) Task -} - -// ITeam combines the AccessController and Lifecycle interfaces to define a complete -// team interface. It ensures that a team has both access control and lifecycle -// management capabilities. -type ITeam interface { - AccessController - Lifecycle -} - -var teams avl.Tree // std.Address -> *Team - -// reHomeRealm validate thehome realm path format `xxx.xx/r//home`. -// XXX: Use somthing simpler -var reHomeRealm = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_]+/home$`) - -// Register creates and registers a new team in the `r/sys/teams` realm, allowing a registered -// user to transform into a team by publishing a contract that registers members in a registry. -// -// The `Register` function verifies that the caller is a valid home path (`r//home`), -// using `` as the team name. -func Register(iteam ITeam) *Team { - caller := std.GetOrigCaller() - realm := std.PrevRealm() - - // Check if caller is not already registered as a team - if teams.Has(caller.String()) { - panic("team already registered: " + caller.String()) - } - - // Check if origin caller is a home path - if !reHomeRealm.MatchString(realm.PkgPath()) { - panic("cannot register a team outside a home realm") - } - - // Initialize the team - team := &Team{ - address: caller, - Lifecycle: iteam, - AccessController: iteam, - } - // Assert that team implementation correctly uses fallback - // XXX: do we want this? - // It asserts that caller has a minimal implementation of Update - assertDefaultUpdate(team) - - if initTask := team.Init(); initTask != nil { - team.performTasks(caller, initTask) - } - - // All set, register the team - teams.Set(caller.String(), team) - return team -} - -// Get retrieves a registered team by address. -func Get(teamAddr std.Address) *Team { - if t, ok := teams.Get(teamAddr.String()); ok { - return t.(*Team) - } - return nil -} - -// IsRegister checks if a team is already registered by address. -func IsRegister(teamAddr std.Address) bool { - return teams.Has(teamAddr.String()) -} diff --git a/examples/gno.land/r/sys/teams/teams_ownable.gno b/examples/gno.land/r/sys/teams/teams_ownable.gno deleted file mode 100644 index 9d42a9d18da..00000000000 --- a/examples/gno.land/r/sys/teams/teams_ownable.gno +++ /dev/null @@ -1,59 +0,0 @@ -package teams - -import ( - "std" - - "gno.land/p/demo/ownable" -) - -type OwnableTeam struct { - *OwnableAccessController -} - -func NewOwnableTeam(owner *ownable.Ownable) ITeam { - return &OwnableTeam{ - OwnableAccessController: NewOwnableAccessController(owner), - } -} - -func (o *OwnableTeam) Init() Task { - return ChainTasks( - AddMemberTask(o.Owner()), - // Burn team address, so only owner can control the team - BurnTeamAddressTask, - ) -} - -func (o *OwnableTeam) ApplyUpdate(cmd Cmd) Task { - return ApplyDefault(cmd) -} - -type OwnableAccessController struct { - *ownable.Ownable - - EnableAddMember bool - EnableRemoveMember bool - EnableAddPackage bool -} - -func NewOwnableAccessController(ownable *ownable.Ownable) *OwnableAccessController { - return &OwnableAccessController{Ownable: ownable} -} - -func (o *OwnableAccessController) CanRun(member std.Address, cmd Cmd) bool { - - if o.Ownable.Owner() == member { // All mighty owner - return true - } - - switch cmd.(type) { - case AddMemberCmd: - return o.EnableAddMember - case RemoveMemberCmd: - return o.EnableRemoveMember - case AddPackageCmd: - return o.EnableAddPackage - } - - return false -} diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno deleted file mode 100644 index e403cc9bd87..00000000000 --- a/examples/gno.land/r/sys/teams/teams_test.gno +++ /dev/null @@ -1,3 +0,0 @@ -package teams - -// XXX: TODO diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno deleted file mode 100644 index 0a7e75b2c10..00000000000 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ /dev/null @@ -1,42 +0,0 @@ -// PKGPATH: gno.land/r/myteam/home - -// This the most minimal usage of team -package home - -import ( - "gno.land/p/demo/testutils" - "gno.land/r/sys/teams" -) - -var myteam *teams.Team - -// var myteam *Team -func init() { - myteam = teams.Register(nil) -} - -func main() { - alice := testutils.TestAddress("alice") - teamAddress := myteam.Address() - - // Setup team user address - println("team address is team address:", myteam.IsTeamAddress(teamAddress)) - println("team address is not member:", !myteam.HasMember(teamAddress)) - println("team address can add package:", myteam.CanAddPackage(teamAddress)) - println("team address can add member alice:", myteam.CanAddMember(teamAddress, alice)) - println("alice is not member:", !myteam.HasMember(alice)) - - println(" -> adding alice as member") - myteam.AddMember(alice) - - println("alice is now a member:", myteam.HasMember(alice)) -} - -// Output: -// team address is team address: true -// team address is not member: true -// team address can add package: true -// team address can add member alice: true -// alice is not member: true -// -> adding alice as member -// alice is now a member: true diff --git a/examples/gno.land/r/sys/teams/z_9_filetest.gno b/examples/gno.land/r/sys/teams/z_9_filetest.gno deleted file mode 100644 index a85353177d5..00000000000 --- a/examples/gno.land/r/sys/teams/z_9_filetest.gno +++ /dev/null @@ -1,211 +0,0 @@ -// PKGPATH: gno.land/r/myteam/home - -// This is an example of a more advanced usage of team -package home - -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/testutils" - "gno.land/r/sys/teams" -) - -// var myteam *Team - -type Level int - -const ( - LevelUnknown Level = iota // lowest - Level1 - Level2 - Level3 - Level4 -) - -type MyTeam struct { - *teams.Team - address std.Address - levels avl.Tree // std.Address -> Level -} - -func (m *MyTeam) Init() teams.Task { - return nil -} - -func (m *MyTeam) CanRun(member std.Address, cmd teams.Cmd) bool { - level := m.GetLevel(member) - shouldBeLevelMinimum := func(target Level) bool { - return level >= target - } - - // Team action - switch typ := cmd.(type) { - case SetLevelCmd: - // Can only set level on inferior level - return level > typ.Level - case teams.RemoveMemberCmd: - return level >= Level3 - case teams.AddMemberCmd: - return level >= Level2 - case teams.AddPackageCmd: - return level >= Level1 - } - - return false -} - -type SetLevelCmd struct { - Member std.Address - Level -} - -func (SetLevelCmd) Name() string { return "SetLevel" } - -func (m *MyTeam) ApplyUpdate(cmd teams.Cmd) teams.Task { - switch typ := cmd.(type) { - case SetLevelCmd: - return teams.CreateTask(func(_ *teams.Team) teams.Cmd { - mkey := typ.Member.String() - m.levels.Set(mkey, typ.Level) - return nil - }) - case teams.AddMemberCmd: - return teams.ChainTasks( - // Add a new member - teams.AddMemberTask(typ.Member), - // Promote it to level 1 - teams.CreateTaskCmd(SetLevelCmd{ - Member: typ.Member, - Level: Level1, - }), - ) - case teams.RemoveMemberCmd: - return teams.ChainTasks( - // Add a new member - teams.RemoveMemberTask(typ.Member), - // Promote it to level 1 - teams.CreateTaskCmd(SetLevelCmd{ - Member: typ.Member, - Level: LevelUnknown, - }), - ) - - } - - return teams.ApplyDefault(cmd) -} - -func (m *MyTeam) GetLevel(member std.Address) Level { - if level, ok := m.levels.Get(member.String()); ok { - return level.(Level) - } - return LevelUnknown -} - -func (m *MyTeam) SetLevel(member std.Address, level Level) { - m.Team.Run(SetLevelCmd{ - Member: member, - Level: level, - }) -} - -var myteam MyTeam - -func init() { - // inherit all methods from Team - myteam.Team = teams.Register(&myteam) -} - -func main() { - println("* registered myteam") - myteamUser := myteam.Address() - - // Setup team user address - println("myteamUser is team address:", myteam.IsTeamAddress(myteamUser)) - println("myteamUser is not member:", !myteam.HasMember(myteamUser)) - println("myteamUser has not level:", myteam.GetLevel(myteamUser) == LevelUnknown) - println("myteamUser can add package:", myteam.CanAddPackage(myteamUser)) - - // Setup test users - alice := testutils.TestAddress("alice") - bob := testutils.TestAddress("bob") - - println("alice cannot add a package:", !myteam.CanAddPackage(alice)) - - // Register alice to the team - println(" -> adding alice as a member") - myteam.AddMember(alice) - - println("alice is member:", myteam.HasMember(alice)) - println("alice is level_1:", myteam.GetLevel(alice) == Level1) - println("alice can add package:", myteam.CanAddPackage(alice)) - println("bob cannot add package:", !myteam.CanAddPackage(bob)) - // Alice should not be able to add a member on level1 - println("as level_1, alice cannot add bob as member:", !myteam.CanAddMember(alice, bob)) - - // Update alice to Level4 - println(" -> setting alice to level 4") - myteam.SetLevel(alice, Level4) - - println("alice is level_4:", myteam.GetLevel(alice) == Level4) - println("alice can add bob as member:", myteam.CanAddMember(alice, bob)) - - // Burn team address - println(" -> burn team address") - myteam.BurnTeamAddress() - - println("myteamUser is not member:", !myteam.HasMember(myteamUser)) - println("myteamUser is level_Unknown:", myteam.GetLevel(myteamUser) == LevelUnknown) - println("myteamUser cannot add package:", !myteam.CanAddPackage(myteamUser)) - - println("alice is still level_4:", myteam.GetLevel(alice) == Level4) - println("alice can still add bob as member:", myteam.CanAddMember(alice, bob)) - - // Set caller to alice - println(" -> setting alice as origin caller") - std.TestSetOrigCaller(alice) - - // alice add member bob - println(" -> alice add bob as member") - myteam.AddMember(bob) - - println("bob is member:", myteam.HasMember(bob)) - println("bob is level_1:", myteam.GetLevel(bob) == Level1) - println("bob can add package:", myteam.CanAddPackage(bob)) - - println(" -> removing bob") - myteam.RemoveMember(bob) - println("bob is not member:", !myteam.HasMember(bob)) - -} - -// Output: -// * registered myteam -// myteamUser is team address: true -// myteamUser is not member: true -// myteamUser has not level: true -// myteamUser can add package: true -// alice cannot add a package: true -// -> adding alice as a member -// alice is member: true -// alice is level_1: true -// alice can add package: true -// bob cannot add package: true -// as level_1, alice cannot add bob as member: true -// -> setting alice to level 4 -// alice is level_4: true -// alice can add bob as member: true -// -> burn team address -// myteamUser is not member: true -// myteamUser is level_Unknown: true -// myteamUser cannot add package: true -// alice is still level_4: true -// alice can still add bob as member: true -// -> setting alice as origin caller -// -> alice add bob as member -// bob is member: true -// bob is level_1: true -// bob can add package: true -// -> removing bob -// bob is not member: true From 74ac3152f89db03fcaaf29fe62c891e96eacee75 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:08:39 +0100 Subject: [PATCH 11/18] feat: add teams basic Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/team_basic.gno | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 examples/gno.land/r/sys/teams/team_basic.gno diff --git a/examples/gno.land/r/sys/teams/team_basic.gno b/examples/gno.land/r/sys/teams/team_basic.gno new file mode 100644 index 00000000000..f702eca69e4 --- /dev/null +++ b/examples/gno.land/r/sys/teams/team_basic.gno @@ -0,0 +1,80 @@ +package teams + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Permissions struct{ CanAddPackage bool } + +// BasicTeam is the default team implementation +type BasicTeam struct { + members avl.Tree // std.Address.String() -> Perms +} + +// Can checks permissions for a specific action +func (t *BasicTeam) Can(member std.Address, action Action) bool { + val, exists := t.members.Get(member.String()) + if !exists { + return false + } + + perms := val.(Permissions) + + switch action.(type) { + case ActionAddPackage: + return perms.CanAddPackage + default: + return false + } +} + +// Size returns total number of team members +func (t *BasicTeam) Size() int { + return t.members.Size() +} + +// HasMember checks if address belongs to the team +func (t *BasicTeam) HasMember(member std.Address) bool { + return t.members.Has(member.String()) +} + +// Page retrieves members with pagination support +func (t *BasicTeam) Page(pageNumber, pageSize int) []std.Address { + start := pageNumber * pageSize + end := start + pageSize + size := t.members.Size() + + if start >= size { + return nil + } + if end > size { + end = size + } + + count := end - start + result := make([]std.Address, 0, count) + t.members.IterateByOffset(start, count, func(key string, perm interface{}) bool { + result = append(result, std.Address(key)) + return true + }) + return result +} + +// SetMember updates permissions for a team member +// Only team address can set member permission +func OwnerSetMember(member std.Address, perms Permissions) { + caller := std.GetOrigCaller() + val, exists := teams.Get(caller.String()) + if !exists { + panic(ErrInvalidTeam) + } + + team, ok := val.(*BasicTeam) + if !ok { + panic("unable to set permission on not-managed team by `sys/teams`") + } + + team.members.Set(member.String(), perms) +} From 9c2088135d6a4abaa066012a8d069d065f1e090b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 13 Feb 2025 08:01:46 +0100 Subject: [PATCH 12/18] fix: mod Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/sys/teams/gno.mod b/examples/gno.land/r/sys/teams/gno.mod index 1d7dc7853c2..4afbd1a0fe9 100644 --- a/examples/gno.land/r/sys/teams/gno.mod +++ b/examples/gno.land/r/sys/teams/gno.mod @@ -1 +1 @@ -module gno.land/r/sys/teams2 \ No newline at end of file +module gno.land/r/sys/teams \ No newline at end of file From ed8ed49211bea459b7d3bd845f7bfb5079c6d1e5 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:42:20 +0100 Subject: [PATCH 13/18] fix: add missing method Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/team.gno | 48 ++++++++++++++++++-------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno index 1fc41a9968c..a7c1fb4efe8 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/team.gno @@ -25,21 +25,23 @@ type ( // NOTE: More actions to come... ) -// Team defines the core capabilities for team management +// Team defines the core capabilities for team management. type Team interface { Can(member std.Address, action Action) bool // Members listing + // XXX This should probably not be part of the interface, but should still be + // something exposed through `sys/team` Size() int HasMember(member std.Address) bool PageMember(pageNumber, pageSize int) []std.Address } -// Register creates a new team from a home realm +// Register creates a new team from a home realm. func Register() { caller := std.GetOrigCaller() if teams.Has(caller.String()) { - panic("team already exists: " + caller.String()) + panic("team already register: " + caller.String()) } prev := std.PrevRealm() @@ -59,22 +61,38 @@ func Get(team std.Address) Team { return nil } -// SetMember updates permissions for a team member -func HasMember(team, member std.Address) bool { - caller := std.GetOrigCaller() - val, exists := teams.Get(caller.String()) - if !exists { - panic(ErrInvalidTeam) +// TeamSize returns the number of members in the specified team. +func TeamSize(team std.Address) int { + if t := Get(team); t != nil { + return t.Size() + } + + panic(ErrInvalidTeam) +} + +// TeamHasMember checks if a member is part of the specified team. +func TeamHasMember(team std.Address, member std.Address) bool { + if t := Get(team); t != nil { + return t.HasMember(member) } - return val.(Team).HasMember(member) + panic(ErrInvalidTeam) } -// CanTeamMemberDo checks permissions across teams +// TeamPageMember returns a paginated list of members from the specified team. +func TeamPageMember(team std.Address, pageNumber, pageSize int) []std.Address { + if t := Get(team); t != nil { + return t.PageMember(pageNumber, pageSize) + } + + panic(ErrInvalidTeam) +} + +// CanTeamMemberDo checks permissions across teams. func MemberCanDo(team, member std.Address, action string) bool { - val, exists := teams.Get(team.String()) - if !exists { - return false + if t := Get(team); t != nil { + return t.Can(member, action) } - return val.(Team).Can(member, action) + + panic(ErrInvalidTeam) } From 2ce826a521f517b44cdd94064807d537736359de Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:21:37 +0100 Subject: [PATCH 14/18] wip: basic ac Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/basic_ac.gno | 53 +++++++++ examples/gno.land/r/sys/teams/team.gno | 108 ++++++++++++++----- examples/gno.land/r/sys/teams/team_basic.gno | 80 -------------- 3 files changed, 133 insertions(+), 108 deletions(-) create mode 100644 examples/gno.land/r/sys/teams/basic_ac.gno delete mode 100644 examples/gno.land/r/sys/teams/team_basic.gno diff --git a/examples/gno.land/r/sys/teams/basic_ac.gno b/examples/gno.land/r/sys/teams/basic_ac.gno new file mode 100644 index 00000000000..779858117c8 --- /dev/null +++ b/examples/gno.land/r/sys/teams/basic_ac.gno @@ -0,0 +1,53 @@ +package teams + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Permissions struct{ CanAddPackage bool } + +// BasicAccessController is the default Access Controller implementation +type BasicAccessController struct { + members avl.Tree // std.Address -> Perms +} + +// Can checks permissions for a specific action +func (ac *BasicAccessController) Can(member std.Address, action Action) bool { + val, exists := ac.members.Get(member.String()) + if !exists { + return false + } + + perms := val.(Permissions) + + switch action.(type) { + case ActionAddPackage: + return perms.CanAddPackage + default: + return false + } +} + +func (ac *BasicAccessController) setPermission(member std.Address, perms Permissions) { + ac.members.Set(member.String(), perms) +} + +// SetMember updates permissions for a team member +// Only team address can set member permission +func OwnerSetMember(member std.Address, perms Permissions) { + caller := std.GetOrigCaller() + team := Get(caller) + if team == nil { + panic(ErrInvalidTeam) + } + + ac, ok := team.AccessController.(*BasicAccessController) + if !ok { + panic("AccessControler not managed by `sys/teams`") + } + + team.Register(member) + ac.setPermission(member, perms) +} diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno index a7c1fb4efe8..17eaf943731 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/team.gno @@ -8,6 +8,8 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" + "gno.land/p/moul/addrset" ) var teams avl.Tree // std.Address -> Team @@ -26,15 +28,74 @@ type ( ) // Team defines the core capabilities for team management. -type Team interface { - Can(member std.Address, action Action) bool - - // Members listing - // XXX This should probably not be part of the interface, but should still be - // something exposed through `sys/team` - Size() int - HasMember(member std.Address) bool - PageMember(pageNumber, pageSize int) []std.Address +type AccessController interface { + Can(member std.Address, do Action) bool +} + +type Team struct { + AccessController + + address std.Address + members addrset.Set +} + +// HasMember checks if address belongs to the team +func (t *Team) Has(member std.Address) bool { + return t.members.Has(member) +} + +// Members size returns total number of team members +func (t *Team) Size() int { + return t.members.Size() +} + +// Page retrieves members with pagination support +func (t *Team) Page(pageNumber, pageSize int) []std.Address { + start := pageNumber * pageSize + end := start + pageSize + size := t.members.Size() + + if start >= size { + return nil + } + if end > size { + end = size + } + + count := end - start + result := make([]std.Address, 0, count) + t.members.IterateByOffset(start, count, func(key std.Address) bool { + result = append(result, std.Address(key)) + return false + }) + + return result +} + +func (t *Team) Tree() *rotree.ReadOnlyTree { + tree := t.members.Tree().(*avl.Tree) + return rotree.Wrap(tree, nil) +} + +// Admin method +func (t *Team) Register(member std.Address) { + t.assertTeamAddress() + t.members.Add(member) + return +} + +func (t *Team) SetAccessController(ac AccessController) { + t.assertTeamAddress() + t.AccessController = ac + return +} + +func (t *Team) assertTeamAddress() { + caller := std.GetOrigCaller() + if caller != t.address { + panic(ErrUnauthorized) + } + } // Register creates a new team from a home realm. @@ -50,46 +111,37 @@ func Register() { panic(ErrNotHomeCaller) } - teams.Set(caller.String(), &BasicTeam{}) + teams.Set(caller.String(), &BasicAccessController{}) } -func Get(team std.Address) Team { +func Get(team std.Address) *Team { if val, ok := teams.Get(team.String()); ok { - return val.(Team) + return val.(*Team) } return nil } -// TeamSize returns the number of members in the specified team. -func TeamSize(team std.Address) int { - if t := Get(team); t != nil { - return t.Size() - } - - panic(ErrInvalidTeam) -} - -// TeamHasMember checks if a member is part of the specified team. -func TeamHasMember(team std.Address, member std.Address) bool { +// Has checks if a member is part of the specified team. +func Has(team std.Address, member std.Address) bool { if t := Get(team); t != nil { - return t.HasMember(member) + return t.Has(member) } panic(ErrInvalidTeam) } -// TeamPageMember returns a paginated list of members from the specified team. -func TeamPageMember(team std.Address, pageNumber, pageSize int) []std.Address { +// Page returns a paginated list of members from the specified team. +func Page(team std.Address, pageNumber, pageSize int) []std.Address { if t := Get(team); t != nil { - return t.PageMember(pageNumber, pageSize) + return t.Page(pageNumber, pageSize) } panic(ErrInvalidTeam) } // CanTeamMemberDo checks permissions across teams. -func MemberCanDo(team, member std.Address, action string) bool { +func Can(team, member std.Address, action string) bool { if t := Get(team); t != nil { return t.Can(member, action) } diff --git a/examples/gno.land/r/sys/teams/team_basic.gno b/examples/gno.land/r/sys/teams/team_basic.gno deleted file mode 100644 index f702eca69e4..00000000000 --- a/examples/gno.land/r/sys/teams/team_basic.gno +++ /dev/null @@ -1,80 +0,0 @@ -package teams - -import ( - "std" - - "gno.land/p/demo/avl" -) - -type Permissions struct{ CanAddPackage bool } - -// BasicTeam is the default team implementation -type BasicTeam struct { - members avl.Tree // std.Address.String() -> Perms -} - -// Can checks permissions for a specific action -func (t *BasicTeam) Can(member std.Address, action Action) bool { - val, exists := t.members.Get(member.String()) - if !exists { - return false - } - - perms := val.(Permissions) - - switch action.(type) { - case ActionAddPackage: - return perms.CanAddPackage - default: - return false - } -} - -// Size returns total number of team members -func (t *BasicTeam) Size() int { - return t.members.Size() -} - -// HasMember checks if address belongs to the team -func (t *BasicTeam) HasMember(member std.Address) bool { - return t.members.Has(member.String()) -} - -// Page retrieves members with pagination support -func (t *BasicTeam) Page(pageNumber, pageSize int) []std.Address { - start := pageNumber * pageSize - end := start + pageSize - size := t.members.Size() - - if start >= size { - return nil - } - if end > size { - end = size - } - - count := end - start - result := make([]std.Address, 0, count) - t.members.IterateByOffset(start, count, func(key string, perm interface{}) bool { - result = append(result, std.Address(key)) - return true - }) - return result -} - -// SetMember updates permissions for a team member -// Only team address can set member permission -func OwnerSetMember(member std.Address, perms Permissions) { - caller := std.GetOrigCaller() - val, exists := teams.Get(caller.String()) - if !exists { - panic(ErrInvalidTeam) - } - - team, ok := val.(*BasicTeam) - if !ok { - panic("unable to set permission on not-managed team by `sys/teams`") - } - - team.members.Set(member.String(), perms) -} From 38a13557bb0aedf80f02376c6ff191c6114e59b1 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:22:33 +0100 Subject: [PATCH 15/18] -- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/basic_ac.gno | 3 +-- examples/gno.land/r/sys/teams/team.gno | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/sys/teams/basic_ac.gno b/examples/gno.land/r/sys/teams/basic_ac.gno index 779858117c8..c2ba24e3cb6 100644 --- a/examples/gno.land/r/sys/teams/basic_ac.gno +++ b/examples/gno.land/r/sys/teams/basic_ac.gno @@ -36,7 +36,7 @@ func (ac *BasicAccessController) setPermission(member std.Address, perms Permiss // SetMember updates permissions for a team member // Only team address can set member permission -func OwnerSetMember(member std.Address, perms Permissions) { +func SetMember(member std.Address, perms Permissions) { caller := std.GetOrigCaller() team := Get(caller) if team == nil { @@ -50,4 +50,3 @@ func OwnerSetMember(member std.Address, perms Permissions) { team.Register(member) ac.setPermission(member, perms) -} diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/team.gno index 17eaf943731..65f08288f4c 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/team.gno @@ -72,6 +72,16 @@ func (t *Team) Page(pageNumber, pageSize int) []std.Address { return result } +func (t *Team) Can(member std.Address, do Action) bool { + if !t.Has(member) { + panic(ErrUnauthorized) + } + + return t.AccessController.Can(member, do) +} + +// Admin method + func (t *Team) Tree() *rotree.ReadOnlyTree { tree := t.members.Tree().(*avl.Tree) return rotree.Wrap(tree, nil) From 773f9e02e212dfcecea4760c3015910508a0990a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:04:20 +0100 Subject: [PATCH 16/18] feat: update and add tests Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/basic_ac.gno | 52 ------ .../r/sys/teams/{team.gno => teams.gno} | 96 +++++++---- examples/gno.land/r/sys/teams/teams_test.gno | 157 ++++++++++++++++++ .../gno.land/r/sys/teams/z_1_filetest.gno | 57 +++++++ .../gno.land/r/sys/teams/z_2_filetest.gno | 57 +++++++ examples/gno.land/r/sys/users/verify.gno | 101 ++++++----- gno.land/pkg/sdk/vm/keeper.go | 3 +- 7 files changed, 405 insertions(+), 118 deletions(-) delete mode 100644 examples/gno.land/r/sys/teams/basic_ac.gno rename examples/gno.land/r/sys/teams/{team.gno => teams.gno} (53%) create mode 100644 examples/gno.land/r/sys/teams/teams_test.gno create mode 100644 examples/gno.land/r/sys/teams/z_1_filetest.gno create mode 100644 examples/gno.land/r/sys/teams/z_2_filetest.gno diff --git a/examples/gno.land/r/sys/teams/basic_ac.gno b/examples/gno.land/r/sys/teams/basic_ac.gno deleted file mode 100644 index c2ba24e3cb6..00000000000 --- a/examples/gno.land/r/sys/teams/basic_ac.gno +++ /dev/null @@ -1,52 +0,0 @@ -package teams - -import ( - "std" - - "gno.land/p/demo/avl" -) - -type Permissions struct{ CanAddPackage bool } - -// BasicAccessController is the default Access Controller implementation -type BasicAccessController struct { - members avl.Tree // std.Address -> Perms -} - -// Can checks permissions for a specific action -func (ac *BasicAccessController) Can(member std.Address, action Action) bool { - val, exists := ac.members.Get(member.String()) - if !exists { - return false - } - - perms := val.(Permissions) - - switch action.(type) { - case ActionAddPackage: - return perms.CanAddPackage - default: - return false - } -} - -func (ac *BasicAccessController) setPermission(member std.Address, perms Permissions) { - ac.members.Set(member.String(), perms) -} - -// SetMember updates permissions for a team member -// Only team address can set member permission -func SetMember(member std.Address, perms Permissions) { - caller := std.GetOrigCaller() - team := Get(caller) - if team == nil { - panic(ErrInvalidTeam) - } - - ac, ok := team.AccessController.(*BasicAccessController) - if !ok { - panic("AccessControler not managed by `sys/teams`") - } - - team.Register(member) - ac.setPermission(member, perms) diff --git a/examples/gno.land/r/sys/teams/team.gno b/examples/gno.land/r/sys/teams/teams.gno similarity index 53% rename from examples/gno.land/r/sys/teams/team.gno rename to examples/gno.land/r/sys/teams/teams.gno index 65f08288f4c..c21d0e8a6f5 100644 --- a/examples/gno.land/r/sys/teams/team.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -9,15 +9,15 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/avl/rotree" - "gno.land/p/moul/addrset" ) var teams avl.Tree // std.Address -> Team var ( - ErrNotHomeCaller = errors.New("must be called from r//home realm") - ErrUnauthorized = errors.New("unauthorized operation") - ErrInvalidTeam = errors.New("invalid or unknown team") + ErrNotHomeCaller = errors.New("must be called from r//home realm") + ErrUnauthorized = errors.New("unauthorized operation") + ErrAlreadyRegistered = errors.New("team already registered") + ErrInvalidTeam = errors.New("invalid or unknown team") ) type Action interface{} @@ -27,7 +27,9 @@ type ( // NOTE: More actions to come... ) -// Team defines the core capabilities for team management. +type Permissions struct{ CanAddPackage bool } + +// AccessController overriding access capabilities for team. type AccessController interface { Can(member std.Address, do Action) bool } @@ -36,12 +38,12 @@ type Team struct { AccessController address std.Address - members addrset.Set + members avl.Tree // Address -> Permissions // members default permissions } // HasMember checks if address belongs to the team func (t *Team) Has(member std.Address) bool { - return t.members.Has(member) + return t.members.Has(member.String()) } // Members size returns total number of team members @@ -64,7 +66,7 @@ func (t *Team) Page(pageNumber, pageSize int) []std.Address { count := end - start result := make([]std.Address, 0, count) - t.members.IterateByOffset(start, count, func(key std.Address) bool { + t.members.IterateByOffset(start, count, func(key string, _ interface{}) bool { result = append(result, std.Address(key)) return false }) @@ -73,55 +75,85 @@ func (t *Team) Page(pageNumber, pageSize int) []std.Address { } func (t *Team) Can(member std.Address, do Action) bool { - if !t.Has(member) { - panic(ErrUnauthorized) + if t.IsTeamAddress(member) { // all mighty team address + return true + } + + var perms Permissions + + // If member is not known, deny systematically + val, exists := t.members.Get(member.String()) + if !exists { + return false + } + + perms = val.(Permissions) + + if t.AccessController != nil { + return t.AccessController.Can(member, do) } - return t.AccessController.Can(member, do) + // Check default permission + switch do.(type) { + case ActionAddPackage: + return perms.CanAddPackage + default: + return false + } } // Admin method func (t *Team) Tree() *rotree.ReadOnlyTree { - tree := t.members.Tree().(*avl.Tree) - return rotree.Wrap(tree, nil) + return rotree.Wrap(&t.members, nil) +} + +func (t *Team) Set(member std.Address, perms Permissions) (updated bool) { + t.assertCallerIsTeamAddress() + return t.members.Set(member.String(), perms) } -// Admin method func (t *Team) Register(member std.Address) { - t.assertTeamAddress() - t.members.Add(member) - return + t.assertCallerIsTeamAddress() + if !t.members.Has(member.String()) { + // we don't want to override actual permission if already set + t.members.Set(member.String(), Permissions{}) + } } func (t *Team) SetAccessController(ac AccessController) { - t.assertTeamAddress() + t.assertCallerIsTeamAddress() t.AccessController = ac return } -func (t *Team) assertTeamAddress() { - caller := std.GetOrigCaller() - if caller != t.address { +func (t *Team) IsTeamAddress(addr std.Address) bool { + return addr == t.address +} + +func (t *Team) assertCallerIsTeamAddress() { + if !t.IsTeamAddress(std.GetOrigCaller()) { panic(ErrUnauthorized) } - } // Register creates a new team from a home realm. -func Register() { +func Register() *Team { caller := std.GetOrigCaller() if teams.Has(caller.String()) { panic("team already register: " + caller.String()) } - prev := std.PrevRealm() - pathParts := strings.Split(prev.PkgPath(), "/") - if len(pathParts) != 4 || pathParts[2] != caller.String() || pathParts[3] != "home" { + prevPkg := std.PrevRealm().PkgPath() + pathParts := strings.Split(prevPkg, "/") + if len(pathParts) != 4 || !strings.HasPrefix(prevPkg, "gno.land/r/") || pathParts[3] != "home" { panic(ErrNotHomeCaller) } - teams.Set(caller.String(), &BasicAccessController{}) + team := &Team{address: caller} + teams.Set(caller.String(), team) + return team + } func Get(team std.Address) *Team { @@ -151,10 +183,18 @@ func Page(team std.Address, pageNumber, pageSize int) []std.Address { } // CanTeamMemberDo checks permissions across teams. -func Can(team, member std.Address, action string) bool { +func Can(team, member std.Address, action Action) bool { if t := Get(team); t != nil { return t.Can(member, action) } panic(ErrInvalidTeam) } + +func SetMember(team, member std.Address, perms Permissions) (updated bool) { + if t := Get(team); t != nil { + return t.Set(member, perms) + } + + panic(ErrInvalidTeam) +} diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno new file mode 100644 index 00000000000..c10a7eaaa72 --- /dev/null +++ b/examples/gno.land/r/sys/teams/teams_test.gno @@ -0,0 +1,157 @@ +// teams_test.gno +package teams + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" +) + +var ( + teamAddr = testutils.TestAddress("team1") + memberAddr = testutils.TestAddress("member1") +) + +func TestRegister(t *testing.T) { + defer cleanupTeam() + + // Valid registration + func() { + caller := std.GetOrigCaller() + defer std.TestSetOrigCaller(caller) + + // Setup valid realm context + std.TestSetOrigCaller(teamAddr) + std.TestSetRealm(std.NewCodeRealm("gno.land/r/team1/home")) + + team := Register() + uassert.True(t, team != nil) + uassert.True(t, team == Get(teamAddr)) + }() + + // Test duplicate registration panic + uassert.PanicsWithMessage(t, "team already register: "+teamAddr.String(), func() { + std.TestSetOrigCaller(teamAddr) + std.TestSetRealm(std.NewCodeRealm("gno.land/r/team1/home")) + Register() + }) +} + +func TestMemberManagement(t *testing.T) { + setupTeam() + defer cleanupTeam() + + // Test initial state + uassert.True(t, !Has(teamAddr, memberAddr)) + uassert.Equal(t, 0, Size(teamAddr)) + + // Add member + asTeam(func() { + updated := SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + uassert.False(t, updated, "should be new") + }) + + uassert.True(t, Has(teamAddr, memberAddr), "should have member") + uassert.Equal(t, 1, Size(teamAddr)) + + // Test duplicate set + asTeam(func() { + updated := SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + uassert.True(t, updated, "should be updated") + }) +} + +func TestPermissions(t *testing.T) { + setupTeam() + defer cleanupTeam() + + asTeam(func() { + SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + }) + + // Valid permission + uassert.True(t, Can(teamAddr, memberAddr, ActionAddPackage{})) + + // Invalid permission + uassert.True(t, !Can(teamAddr, memberAddr, "other_action")) + + // Team address has full access + uassert.True(t, Can(teamAddr, teamAddr, ActionAddPackage{})) +} + +func TestPagination(t *testing.T) { + setupTeam() + defer cleanupTeam() + + asTeam(func() { + for i := 1; i <= 5; i++ { + addr := std.Address(ufmt.Sprintf("member%d", i)) + SetMember(teamAddr, addr, Permissions{}) + } + }) + + // Test page 0 + page := Page(teamAddr, 0, 2) + uassert.Equal(t, 2, len(page)) + uassert.Equal(t, "member1", page[0].String()) + uassert.Equal(t, "member2", page[1].String()) + + // Test page 2 + page = Page(teamAddr, 2, 2) + uassert.Equal(t, len(page), 1) + uassert.Equal(t, "member5", page[0].String()) +} + +func TestAccessController(t *testing.T) { + setupTeam() + defer cleanupTeam() + + // Setup custom access controller + ac := &testAC{allowed: memberAddr} + team := Get(teamAddr) + + asTeam(func() { + team.SetAccessController(ac) + team.Register(memberAddr) + + }) + + // Verify custom permissions + uassert.True(t, Can(teamAddr, memberAddr, ActionAddPackage{}), "member addr") + uassert.True(t, !Can(teamAddr, "other_member", ActionAddPackage{}), "other member") +} + +// Helpers +type testAC struct{ allowed std.Address } + +func (ac *testAC) Can(member std.Address, do Action) bool { + return member == ac.allowed +} + +func asTeam(fn func()) { + orig := std.GetOrigCaller() + std.TestSetOrigCaller(teamAddr) + defer std.TestSetOrigCaller(orig) + fn() +} + +func setupTeam() { + asTeam(func() { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/team1/home")) + Register() + }) +} + +func cleanupTeam() { + teams.Remove(teamAddr.String()) +} + +func Size(addr std.Address) int { + if t := Get(addr); t != nil { + return t.Size() + } + return 0 +} diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno new file mode 100644 index 00000000000..837e7eff8db --- /dev/null +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -0,0 +1,57 @@ +// PKGPATH: gno.land/r/myteam/home + +// This the most minimal usage of team +package home + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/sys/teams" +) + +func main() { + myTeamAddress := testutils.TestAddress("myteam") + std.TestSetOrigCaller(myTeamAddress) + + alice := testutils.TestAddress("alice") + + // alice become a team + myteam := teams.Register() + + // Setup team user address + println("team address is team address:", myteam.IsTeamAddress(myTeamAddress)) + println("team address is not member:", !myteam.Has(myTeamAddress)) + println("team address can add package:", myteam.Can(myTeamAddress, teams.ActionAddPackage{})) + println("alice is not member:", !myteam.Has(alice)) + + println(" -> adding alice as member") + myteam.Register(alice) + + println("alice is now a member:", myteam.Has(alice)) + println("alice cannot add a package:", !myteam.Can(alice, teams.ActionAddPackage{})) + + println(" -> setting alice AddPackage permission to true ") + myteam.Set(alice, teams.Permissions{CanAddPackage: true}) + + println("alice can now add a package:", myteam.Can(alice, teams.ActionAddPackage{})) + + println(" -> setting alice AddPackage permission to false ") + myteam.Set(alice, teams.Permissions{CanAddPackage: true}) + + println("alice cannot now add a package anymore:", !myteam.Can(alice, teams.ActionAddPackage{})) + +} + +// Output: +// team address is team address: true +// team address is not member: true +// team address can add package: true +// alice is not member: true +// -> adding alice as member +// alice is now a member: true +// alice cannot add a package: true +// -> setting alice AddPackage permission to true +// alice can now add a package: true +// -> setting alice AddPackage permission to false +// alice cannot now add a package anymore: false diff --git a/examples/gno.land/r/sys/teams/z_2_filetest.gno b/examples/gno.land/r/sys/teams/z_2_filetest.gno new file mode 100644 index 00000000000..6a9a8728518 --- /dev/null +++ b/examples/gno.land/r/sys/teams/z_2_filetest.gno @@ -0,0 +1,57 @@ +// PKGPATH: gno.land/r/myteam/home + +// A more advanced teams usage +package home + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/sys/teams" +) + +func main() { + myTeamAddress := testutils.TestAddress("myteam") + std.TestSetOrigCaller(myTeamAddress) + + alice := testutils.TestAddress("alice") + + // alice become a team + myteam := teams.Register() + + // Setup team user address + println("team address is team address:", myteam.IsTeamAddress(myTeamAddress)) + println("team address is not member:", !myteam.Has(myTeamAddress)) + println("team address can add package:", myteam.Can(myTeamAddress, teams.ActionAddPackage{})) + println("alice is not member:", !myteam.Has(alice)) + + println(" -> adding alice as member") + myteam.Register(alice) + + println("alice is now a member:", myteam.Has(alice)) + println("alice cannot add a package:", !myteam.Can(alice, teams.ActionAddPackage{})) + + println(" -> setting alice AddPackage permission to true ") + myteam.Set(alice, teams.Permissions{CanAddPackage: true}) + + println("alice can now add a package:", myteam.Can(alice, teams.ActionAddPackage{})) + + println(" -> setting alice AddPackage permission to false ") + myteam.Set(alice, teams.Permissions{CanAddPackage: true}) + + println("alice cannot now add a package anymore:", !myteam.Can(alice, teams.ActionAddPackage{})) + +} + +// Output: +// team address is team address: true +// team address is not member: true +// team address can add package: true +// alice is not member: true +// -> adding alice as member +// alice is now a member: true +// alice cannot add a package: true +// -> setting alice AddPackage permission to true +// alice can now add a package: true +// -> setting alice AddPackage permission to false +// alice cannot now add a package anymore: false diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno index ac156a8781e..b597df03698 100644 --- a/examples/gno.land/r/sys/users/verify.gno +++ b/examples/gno.land/r/sys/users/verify.gno @@ -8,83 +8,110 @@ import ( "gno.land/r/sys/teams" ) -const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul +const adminAddress = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul -type VerifyNameFunc func(enabled bool, address std.Address, name string) bool +type ( + VerifyNameFunc func(enabled bool, address std.Address, name string) bool + VerifyPackageFunc func(enabled bool, address std.Address, name, path string) bool +) var ( - owner = ownable.NewWithAddress(admin) // Package owner - checkFunc = VerifyNameByUser // Checking namespace callback - enabled = false // For now this package is disabled by default + owner = ownable.NewWithAddress(adminAddress) + nameCheck = VerifyNameByUser + packageCheck = VerifyPackageAuthorization + packageEnabled = false ) -func IsEnabled() bool { return enabled } - -// This method ensures that the given address has ownership of the given name. -func IsAuthorizedAddressForName(address std.Address, name string) bool { - return checkFunc(enabled, address, name) -} - -// VerifyNameByUser checks from the `users` package that the user has correctly -// registered the given name. -// This function considers as valid an `address` that matches the `name`. +// Core authorization functions func VerifyNameByUser(enable bool, address std.Address, name string) bool { - if !enable { + if checkBasicAuth(enable, address, name) { return true } - // Allow user with their own address as name - if address.String() == name { + if user := users.GetUserByName(name); user != nil { + return user.Address == address + } + + return false +} + +func VerifyPackageAuthorization(enable bool, address std.Address, name, path string) bool { + if checkBasicAuth(enable, address, name) { return true } - if user := users.GetUserByName(name); user != nil { - // Check if team exist first - // XXX: for now a team is still user - if team := teams.Get(user.Address); team != nil { - return team.CanAddPackage(address) - } + user := users.GetUserByName(name) + if user == nil { + return false // user doesn't exist + } - return user.Address == address + // Check if user is a team + if team := teams.Get(user.Address); team != nil { + return team.Can(address, teams.ActionAddPackage{Path: path}) } - return false + // Else user need to own the name + return user.Address == address } -// Admin calls +// Public authorization interfaces +func IsAuthorizedAddressForName(address std.Address, name string) bool { + return nameCheck(packageEnabled, address, name) +} + +func IsAuthorizedToAddPackage(address std.Address, name, path string) bool { + return packageCheck(packageEnabled, address, name, path) +} + +// System status management +func IsEnabled() bool { return packageEnabled } + +// Helper functions +func checkBasicAuth(enable bool, address std.Address, name string) (authorized bool) { + if !enable { + return true + } -// Enable this package. + return address.String() == name +} + +// Administration functions func AdminEnable() { if !owner.CallerIsOwner() { panic(ownable.ErrUnauthorized) } - enabled = true + packageEnabled = true } -// Disable this package. func AdminDisable() { if !owner.CallerIsOwner() { panic(ownable.ErrUnauthorized) } - enabled = false + packageEnabled = false +} + +func AdminUpdateNameCheck(fn VerifyNameFunc) { + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) + } + + nameCheck = fn } -// AdminUpdateVerifyCall updates the method that verifies the namespace. -func AdminUpdateVerifyCall(check VerifyNameFunc) { +func AdminUpdatePackageCheck(fn VerifyPackageFunc) { if !owner.CallerIsOwner() { panic(ownable.ErrUnauthorized) } - checkFunc = check + packageCheck = fn } -// AdminTransferOwnership transfers the ownership to a new owner. -func AdminTransferOwnership(newOwner std.Address) error { +func AdminTransferOwnership(newOwner std.Address) { if !owner.CallerIsOwner() { panic(ownable.ErrUnauthorized) } - return owner.TransferOwnership(newOwner) + owner.TransferOwnership(newOwner) } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index bf16cd44243..10452ae8e1f 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -295,9 +295,10 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add m.SetActivePackage(mpv) m.RunDeclaration(gno.ImportD("users", sysUsersPkg)) x := gno.Call( - gno.Sel(gno.Nx("users"), "IsAuthorizedAddressForName"), + gno.Sel(gno.Nx("users"), "VerifyPackageAuthorization"), gno.Str(creator.String()), gno.Str(username), + gno.Str(pkgPath), ) ret := m.Eval(x) From 78346abcf280c0b772825719cf880b42810b4e86 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:05:29 +0100 Subject: [PATCH 17/18] chore: remove comments (will update later) Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/teams.gno | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index c21d0e8a6f5..9f07c4ecbfe 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -1,5 +1,3 @@ -// Package team provides decentralized team management with permission control. -// Teams are identified by addresses and manage member permissions. package teams import ( From 4fccd42a1d783621d9b2a3451eb80e1231605d56 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:04:22 +0100 Subject: [PATCH 18/18] fix: teams v2 Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/sys/teams/teams.gno | 163 +++++++++++++----- examples/gno.land/r/sys/teams/teams_test.gno | 52 +++--- .../gno.land/r/sys/teams/z_1_filetest.gno | 4 +- .../gno.land/r/sys/teams/z_2_filetest.gno | 57 ------ .../integration/testdata/register_team.txtar | 40 ++--- gno.land/pkg/sdk/vm/keeper.go | 2 +- 6 files changed, 157 insertions(+), 161 deletions(-) delete mode 100644 examples/gno.land/r/sys/teams/z_2_filetest.gno diff --git a/examples/gno.land/r/sys/teams/teams.gno b/examples/gno.land/r/sys/teams/teams.gno index 9f07c4ecbfe..1fc0721f946 100644 --- a/examples/gno.land/r/sys/teams/teams.gno +++ b/examples/gno.land/r/sys/teams/teams.gno @@ -1,3 +1,11 @@ +// Teams are entities that manage members and their permissions for specific actions. +// +// Basic usage: +// +// team := teams.Register() // Called from gno.land/r//home realm +// team.SetMember("member1", "add_package") +// + package teams import ( @@ -9,47 +17,61 @@ import ( "gno.land/p/demo/avl/rotree" ) -var teams avl.Tree // std.Address -> Team +// Global registry of all teams +var teams avl.Tree // std.Address -> *Team var ( ErrNotHomeCaller = errors.New("must be called from r//home realm") ErrUnauthorized = errors.New("unauthorized operation") ErrAlreadyRegistered = errors.New("team already registered") ErrInvalidTeam = errors.New("invalid or unknown team") + ErrInvalidMember = errors.New("invalid or unknown member") + ErrInvalidPermission = errors.New("invalid permission string") ) +// Action represents an operation that requires authorization type Action interface{} type ( + // ActionAddPackage represents permission to add packages to a team's realm ActionAddPackage struct{ Path string } - // NOTE: More actions to come... ) +// Permissions defines default access rights for team members type Permissions struct{ CanAddPackage bool } -// AccessController overriding access capabilities for team. +// AccessController allows custom permission handling for advanced use cases type AccessController interface { + // Can determines if member has permission for specific action Can(member std.Address, do Action) bool } +// Team represents a decentralized organization with member access control type Team struct { - AccessController + AccessController // Optional custom permission handler + + address std.Address // Team's blockchain address + members avl.Tree // Member permissions storage (Address.String() -> Permissions) +} - address std.Address - members avl.Tree // Address -> Permissions // members default permissions +// Address returns the team's address +func (t *Team) Address() std.Address { + return t.address } -// HasMember checks if address belongs to the team +// Has checks if address belongs to the team func (t *Team) Has(member std.Address) bool { return t.members.Has(member.String()) } -// Members size returns total number of team members +// Size returns total number of team members func (t *Team) Size() int { return t.members.Size() } -// Page retrieves members with pagination support +// Page retrieves paginated member list +// pageNumber: 0-based page index +// pageSize: number of members per page func (t *Team) Page(pageNumber, pageSize int) []std.Address { start := pageNumber * pageSize end := start + pageSize @@ -72,26 +94,25 @@ func (t *Team) Page(pageNumber, pageSize int) []std.Address { return result } +// Can checks if member has permission for an action +// Team addresses always have full access func (t *Team) Can(member std.Address, do Action) bool { - if t.IsTeamAddress(member) { // all mighty team address + if t.IsTeamAddress(member) { return true } - var perms Permissions - - // If member is not known, deny systematically val, exists := t.members.Get(member.String()) if !exists { return false } - perms = val.(Permissions) + perms := val.(Permissions) + // Delegate to custom access controller if set if t.AccessController != nil { return t.AccessController.Can(member, do) } - // Check default permission switch do.(type) { case ActionAddPackage: return perms.CanAddPackage @@ -100,46 +121,63 @@ func (t *Team) Can(member std.Address, do Action) bool { } } -// Admin method - +// Tree provides read-only access to member permissions func (t *Team) Tree() *rotree.ReadOnlyTree { return rotree.Wrap(&t.members, nil) } +// IsTeamAddress checks if address matches team's address +func (t *Team) IsTeamAddress(addr std.Address) bool { + return addr == t.address +} + +// Set updates member permissions (team admin only) func (t *Team) Set(member std.Address, perms Permissions) (updated bool) { t.assertCallerIsTeamAddress() + if !member.IsValid() { + panic(ErrInvalidMember) + } return t.members.Set(member.String(), perms) } +// Register adds a member with default permissions (team admin only) func (t *Team) Register(member std.Address) { t.assertCallerIsTeamAddress() + if !member.IsValid() { + panic(ErrInvalidMember) + } if !t.members.Has(member.String()) { - // we don't want to override actual permission if already set t.members.Set(member.String(), Permissions{}) } } +// SetAccessController configures custom permission handler (team admin only) func (t *Team) SetAccessController(ac AccessController) { t.assertCallerIsTeamAddress() t.AccessController = ac - return -} - -func (t *Team) IsTeamAddress(addr std.Address) bool { - return addr == t.address } +// assertCallerIsTeamAddress validates transaction authorization func (t *Team) assertCallerIsTeamAddress() { if !t.IsTeamAddress(std.GetOrigCaller()) { panic(ErrUnauthorized) } } -// Register creates a new team from a home realm. +var teamSingleton *Team // Cached team reference for Myteam() + +// Register creates a new team from a valid home realm: +// Must be called from a package path matching "gno.land/r//home" +// +// Example: +// +// // From gno.land/r/myteam/home +// myteam := teams.Register() +// myteam.Set(member, Permissions{ CanAddPackage: true }) func Register() *Team { caller := std.GetOrigCaller() if teams.Has(caller.String()) { - panic("team already register: " + caller.String()) + panic(ErrAlreadyRegistered) } prevPkg := std.PrevRealm().PkgPath() @@ -150,49 +188,82 @@ func Register() *Team { team := &Team{address: caller} teams.Set(caller.String(), team) + teamSingleton = team // Update singleton return team +} +// My returns the team associated with current transaction caller +// Primarily used for method chaining after registration: +// +// Example: +// +// // From gno.land/r/myteam/home +// teams.Register() +// teams.MyTeam().SetMember("founder", "add_package") +// teams.MyTeam().Page(0, 10) +func MyTeam() *Team { + caller := std.GetOrigCaller() + if teamSingleton == nil || teamSingleton.Address() != caller { + teamSingleton = MustGet(caller) + } + return teamSingleton } -func Get(team std.Address) *Team { +func MustGet(team std.Address) *Team { if val, ok := teams.Get(team.String()); ok { return val.(*Team) } - return nil + panic(ErrInvalidMember) } -// Has checks if a member is part of the specified team. -func Has(team std.Address, member std.Address) bool { - if t := Get(team); t != nil { - return t.Has(member) +// Get retrieves a team by address +func Get(team std.Address) *Team { + if val, ok := teams.Get(team.String()); ok { + return val.(*Team) } - panic(ErrInvalidTeam) + return nil } -// Page returns a paginated list of members from the specified team. -func Page(team std.Address, pageNumber, pageSize int) []std.Address { - if t := Get(team); t != nil { - return t.Page(pageNumber, pageSize) +// SetMember updates permissions using comma-separated string-based flags +// perm can be: +// - "add_package" +func SetMember(member std.Address, perms string) (updated bool) { + var ps Permissions + + for _, perm := range strings.Split(perms, ",") { + switch perm { + case "add_package": + ps.CanAddPackage = true + case "": // Default permissions + default: + panic(ErrInvalidPermission) + } } - panic(ErrInvalidTeam) + return MyTeam().Set(member, ps) } -// CanTeamMemberDo checks permissions across teams. -func Can(team, member std.Address, action Action) bool { - if t := Get(team); t != nil { - return t.Can(member, action) - } +// CheckPermission verifies member access for an action +func HasPermission(team, member std.Address, action Action) bool { + return MustGet(team).Can(member, action) +} + +// HasMember checks membership status +func HasMember(team std.Address, member std.Address) bool { + return MustGet(team).Has(member) +} - panic(ErrInvalidTeam) +// MembersPage returns paginated member list +func MembersPage(team std.Address, pageNumber, pageSize int) []std.Address { + return MustGet(team).Page(pageNumber, pageSize) } -func SetMember(team, member std.Address, perms Permissions) (updated bool) { +func Size(team std.Address) int { if t := Get(team); t != nil { - return t.Set(member, perms) + return t.Size() } - panic(ErrInvalidTeam) + return 0 } diff --git a/examples/gno.land/r/sys/teams/teams_test.gno b/examples/gno.land/r/sys/teams/teams_test.gno index c10a7eaaa72..40d3c49fc2e 100644 --- a/examples/gno.land/r/sys/teams/teams_test.gno +++ b/examples/gno.land/r/sys/teams/teams_test.gno @@ -33,7 +33,7 @@ func TestRegister(t *testing.T) { }() // Test duplicate registration panic - uassert.PanicsWithMessage(t, "team already register: "+teamAddr.String(), func() { + uassert.PanicsWithMessage(t, ErrAlreadyRegistered.Error(), func() { std.TestSetOrigCaller(teamAddr) std.TestSetRealm(std.NewCodeRealm("gno.land/r/team1/home")) Register() @@ -45,21 +45,21 @@ func TestMemberManagement(t *testing.T) { defer cleanupTeam() // Test initial state - uassert.True(t, !Has(teamAddr, memberAddr)) + uassert.True(t, !HasMember(teamAddr, memberAddr)) uassert.Equal(t, 0, Size(teamAddr)) // Add member asTeam(func() { - updated := SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + updated := MyTeam().Set(memberAddr, DefaultPermissions{CanAddPackage: true}) uassert.False(t, updated, "should be new") }) - uassert.True(t, Has(teamAddr, memberAddr), "should have member") + uassert.True(t, HasMember(teamAddr, memberAddr), "should have member") uassert.Equal(t, 1, Size(teamAddr)) // Test duplicate set asTeam(func() { - updated := SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + updated := MyTeam().Set(memberAddr, DefaultPermissions{CanAddPackage: false}) uassert.True(t, updated, "should be updated") }) } @@ -69,40 +69,44 @@ func TestPermissions(t *testing.T) { defer cleanupTeam() asTeam(func() { - SetMember(teamAddr, memberAddr, Permissions{CanAddPackage: true}) + MyTeam().Set(memberAddr, DefaultPermissions{CanAddPackage: true}) }) // Valid permission - uassert.True(t, Can(teamAddr, memberAddr, ActionAddPackage{})) + uassert.True(t, HasPermission(teamAddr, memberAddr, ActionAddPackage{})) // Invalid permission - uassert.True(t, !Can(teamAddr, memberAddr, "other_action")) + uassert.True(t, !HasPermission(teamAddr, memberAddr, "other_action")) // Team address has full access - uassert.True(t, Can(teamAddr, teamAddr, ActionAddPackage{})) + uassert.True(t, HasPermission(teamAddr, memberAddr, ActionAddPackage{})) } func TestPagination(t *testing.T) { setupTeam() defer cleanupTeam() + var members map[string]std.Address asTeam(func() { + team := MyTeam() for i := 1; i <= 5; i++ { - addr := std.Address(ufmt.Sprintf("member%d", i)) - SetMember(teamAddr, addr, Permissions{}) + name := ufmt.Sprintf("member%d", i) + addr := testutils.TestAddress(name) + members[name] = addr + team.Set(addr, DefaultPermissions{}) } }) // Test page 0 - page := Page(teamAddr, 0, 2) + page := Get(teamAddr).Page(0, 2) uassert.Equal(t, 2, len(page)) - uassert.Equal(t, "member1", page[0].String()) - uassert.Equal(t, "member2", page[1].String()) + uassert.Equal(t, members["member1"], page[0].String()) + uassert.Equal(t, members["member2"], page[1].String()) // Test page 2 - page = Page(teamAddr, 2, 2) + page = Get(teamAddr).Page(2, 2) uassert.Equal(t, len(page), 1) - uassert.Equal(t, "member5", page[0].String()) + uassert.Equal(t, members["member5"], page[0].String()) } func TestAccessController(t *testing.T) { @@ -111,17 +115,16 @@ func TestAccessController(t *testing.T) { // Setup custom access controller ac := &testAC{allowed: memberAddr} - team := Get(teamAddr) asTeam(func() { - team.SetAccessController(ac) - team.Register(memberAddr) + MyTeam().SetAccessController(ac) + MyTeam().Register(memberAddr) }) // Verify custom permissions - uassert.True(t, Can(teamAddr, memberAddr, ActionAddPackage{}), "member addr") - uassert.True(t, !Can(teamAddr, "other_member", ActionAddPackage{}), "other member") + uassert.True(t, HasPermission(teamAddr, memberAddr, ActionAddPackage{}), "member addr") + uassert.True(t, !HasPermission(teamAddr, "other_member", ActionAddPackage{}), "other member") } // Helpers @@ -148,10 +151,3 @@ func setupTeam() { func cleanupTeam() { teams.Remove(teamAddr.String()) } - -func Size(addr std.Address) int { - if t := Get(addr); t != nil { - return t.Size() - } - return 0 -} diff --git a/examples/gno.land/r/sys/teams/z_1_filetest.gno b/examples/gno.land/r/sys/teams/z_1_filetest.gno index 837e7eff8db..46e449fc301 100644 --- a/examples/gno.land/r/sys/teams/z_1_filetest.gno +++ b/examples/gno.land/r/sys/teams/z_1_filetest.gno @@ -37,7 +37,7 @@ func main() { println("alice can now add a package:", myteam.Can(alice, teams.ActionAddPackage{})) println(" -> setting alice AddPackage permission to false ") - myteam.Set(alice, teams.Permissions{CanAddPackage: true}) + myteam.Set(alice, teams.Permissions{CanAddPackage: false}) println("alice cannot now add a package anymore:", !myteam.Can(alice, teams.ActionAddPackage{})) @@ -54,4 +54,4 @@ func main() { // -> setting alice AddPackage permission to true // alice can now add a package: true // -> setting alice AddPackage permission to false -// alice cannot now add a package anymore: false +// alice cannot now add a package anymore: true diff --git a/examples/gno.land/r/sys/teams/z_2_filetest.gno b/examples/gno.land/r/sys/teams/z_2_filetest.gno deleted file mode 100644 index 6a9a8728518..00000000000 --- a/examples/gno.land/r/sys/teams/z_2_filetest.gno +++ /dev/null @@ -1,57 +0,0 @@ -// PKGPATH: gno.land/r/myteam/home - -// A more advanced teams usage -package home - -import ( - "std" - - "gno.land/p/demo/testutils" - "gno.land/r/sys/teams" -) - -func main() { - myTeamAddress := testutils.TestAddress("myteam") - std.TestSetOrigCaller(myTeamAddress) - - alice := testutils.TestAddress("alice") - - // alice become a team - myteam := teams.Register() - - // Setup team user address - println("team address is team address:", myteam.IsTeamAddress(myTeamAddress)) - println("team address is not member:", !myteam.Has(myTeamAddress)) - println("team address can add package:", myteam.Can(myTeamAddress, teams.ActionAddPackage{})) - println("alice is not member:", !myteam.Has(alice)) - - println(" -> adding alice as member") - myteam.Register(alice) - - println("alice is now a member:", myteam.Has(alice)) - println("alice cannot add a package:", !myteam.Can(alice, teams.ActionAddPackage{})) - - println(" -> setting alice AddPackage permission to true ") - myteam.Set(alice, teams.Permissions{CanAddPackage: true}) - - println("alice can now add a package:", myteam.Can(alice, teams.ActionAddPackage{})) - - println(" -> setting alice AddPackage permission to false ") - myteam.Set(alice, teams.Permissions{CanAddPackage: true}) - - println("alice cannot now add a package anymore:", !myteam.Can(alice, teams.ActionAddPackage{})) - -} - -// Output: -// team address is team address: true -// team address is not member: true -// team address can add package: true -// alice is not member: true -// -> adding alice as member -// alice is now a member: true -// alice cannot add a package: true -// -> setting alice AddPackage permission to true -// alice can now add a package: true -// -> setting alice AddPackage permission to false -// alice cannot now add a package anymore: false diff --git a/gno.land/pkg/integration/testdata/register_team.txtar b/gno.land/pkg/integration/testdata/register_team.txtar index d1531369f7a..9b3c6764ce9 100644 --- a/gno.land/pkg/integration/testdata/register_team.txtar +++ b/gno.land/pkg/integration/testdata/register_team.txtar @@ -14,12 +14,12 @@ patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # use our c gnoland start # enable sys/users -gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 1000000 -broadcast -chainid tendermint_test admin +gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test admin stdout 'OK!' # Try to add a pkg an with unregistered user # alice addpkg -> gno.land/r/alice/foo -! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/foo -gas-fee 1000000ugnot -gas-wanted 1000000 -broadcast -chainid=tendermint_test alice +! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/foo -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test alice stderr 'unauthorized user' # Test admin invites alice @@ -44,50 +44,36 @@ stderr 'unauthorized user' # Alice try register a team on a random namespace, should fail # alice addpkg -> gno.land/r/alice/noop -! gnokey maketx addpkg -pkgdir $WORK/home -pkgpath gno.land/r/alice/noop -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test alice -stderr 'cannot register a team outside a home realm' +! gnokey maketx addpkg -pkgdir $WORK/alice/home -pkgpath gno.land/r/alice/noop -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test alice +stderr 'must be called from r//home realm' # Alice try register a team on `home` namespace # alice addpkg -> gno.land/r/alice/home -gnokey maketx addpkg -pkgdir $WORK/home -pkgpath gno.land/r/alice/home -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test alice +gnokey maketx addpkg -pkgdir $WORK/alice/home -pkgpath gno.land/r/alice/home -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test alice stdout 'OK!' # Bob try to add a pkg on alice namespace again # bob addpkg -> gno.land/r/alice/bar -! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bar -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob +! gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bobpkg -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob stderr 'unauthorized user' -# Bob try to add himself as member -# bob call -> alice/home.AddMember(bob) -! gnokey maketx call -pkgpath gno.land/r/alice/home -func AddMember -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $bob_user_addr bob -stderr 'only members or team address can perform commands on the team' - # Alice add bob as member -# alice call -> alice/home.AddMember(bob) -gnokey maketx call -pkgpath gno.land/r/alice/home -func AddMember -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $bob_user_addr alice +# alice call -> sys/teams.SetMember(bob) +gnokey maketx call -pkgpath gno.land/r/sys/teams -func SetMember -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test -args $bob_user_addr -args add_package alice stdout 'OK!' # Bob add a pkg on alice namespace again, success ! -# bob addpkg -> gno.land/r/alice/bar -gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bar -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob +# bob addpkg -> gno.land/r/alice/bobpkg +gnokey maketx addpkg -pkgdir $WORK/mypkg -pkgpath gno.land/r/alice/bobpkg -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test bob stdout 'OK!' --- home/myteam.gno -- +-- alice/home/myteam.gno -- package home -import ( - "std" - "gno.land/r/sys/teams" -) - -var myteam *teams.Team - -func AddMember(addr std.Address) { - myteam.AddMember(addr) -} +import "gno.land/r/sys/teams" func init() { - myteam = teams.Register(nil) + teams.Register() } -- mypkg/mypkg.gno -- diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 10452ae8e1f..9f23dbbd9b1 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -295,7 +295,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add m.SetActivePackage(mpv) m.RunDeclaration(gno.ImportD("users", sysUsersPkg)) x := gno.Call( - gno.Sel(gno.Nx("users"), "VerifyPackageAuthorization"), + gno.Sel(gno.Nx("users"), "IsAuthorizedToAddPackage"), gno.Str(creator.String()), gno.Str(username), gno.Str(pkgPath),