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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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") } }