diff --git a/examples/gno.land/r/system/names/enable.gno b/examples/gno.land/r/system/names/enable.gno new file mode 100644 index 00000000000..b8cba4f892c --- /dev/null +++ b/examples/gno.land/r/system/names/enable.gno @@ -0,0 +1,5 @@ +package names + +// if enabled is true, `HasPerm` will check the permissions of names. +// todo: this is a temporary solution, we should use a more robust +var enabled bool = false diff --git a/examples/gno.land/r/system/names/gno.mod b/examples/gno.land/r/system/names/gno.mod index 31c456f90e0..3a19b02113a 100644 --- a/examples/gno.land/r/system/names/gno.mod +++ b/examples/gno.land/r/system/names/gno.mod @@ -2,4 +2,5 @@ module gno.land/r/system/names require ( "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/ufmt" v0.0.0-latest ) diff --git a/examples/gno.land/r/system/names/names.gno b/examples/gno.land/r/system/names/names.gno index e1f479fc379..265c5e43779 100644 --- a/examples/gno.land/r/system/names/names.gno +++ b/examples/gno.land/r/system/names/names.gno @@ -2,6 +2,8 @@ package names import ( + "errors" + "regexp" "std" "gno.land/p/demo/avl" @@ -12,49 +14,71 @@ import ( // determine if an address can publish a package or not. var namespaces avl.Tree // name(string) -> Space +// TODO: more accurate. +var reNamespace = regexp.MustCompile(`^[a-z][a-z0-9_]{2,30}$`) + type Space struct { Admins []std.Address Editors []std.Address InPause bool } -func Register(namespace string) { - // TODO: input sanitization: - // - already exists / reserved. - // - min/max length, format. - // - fees (dynamic, based on length). - panic("not implemented") +func (s *Space) isAdmin(addr std.Address) bool { + return containsAddress(s.Admins, addr) } -func AddAdmin(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - panic("not implemented") +func (s *Space) addAdmin(newAdmin std.Address) { + if !containsAddress(s.Admins, newAdmin) { + s.Admins = append(s.Admins, newAdmin) + } } -func RemoveAdmin(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - // TODO: check if self. - panic("not implemented") +func (s *Space) removeAdmin(admin std.Address) error { + if len(s.Admins) == 1 { + return errors.New("namespace at least needs one admin") + } + if isCallerAddress(admin) { + return errors.New("cannot remove self") + } + admins := removeAddress(s.Admins, admin) + s.Admins = admins + return nil } -func AddEditor(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - panic("not implemented") +func (s *Space) addEditor(newEditor std.Address) { + if !containsAddress(s.Editors, newEditor) { + s.Editors = append(s.Editors, newEditor) + } } -func RemoveEditor(namespace string, newAdmin std.Address) { - // TODO: assertIsAdmin() - // TODO: check if self. - panic("not implemented") +func (s *Space) removeEditor(editor std.Address) error { + if isCallerAddress(editor) { + return errors.New("cannot remove self") + } + editors := removeAddress(s.Editors, editor) + s.Editors = editors + return nil } -func SetInPause(namespace string, state bool) { - // TODO: assertIsAdmin() - panic("not implemented") +func (s *Space) hasPerm(caller std.Address) bool { + if s.InPause { + return false + } + + if containsAddress(s.Admins, caller) { + return true + } + + if containsAddress(s.Editors, caller) { + return true + } + + return false } -func Render(path string) string { - // TODO: by namespace. - // TODO: by address. - return "not implemented" +func validateNamespace(namespace string) error { + if !reNamespace.MatchString(namespace) { + return errors.New("invalid namespace") + } + return nil } diff --git a/examples/gno.land/r/system/names/public.gno b/examples/gno.land/r/system/names/public.gno new file mode 100644 index 00000000000..8c82f903fce --- /dev/null +++ b/examples/gno.land/r/system/names/public.gno @@ -0,0 +1,124 @@ +package names + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +func Register(namespace string) { + // TODO: input sanitization: + // - already exists / reserved. + // - min/max length, format. + // - fees (dynamic, based on length). + if existsNamespace(namespace) { + panic("namespace already exists") + } + + err := validateNamespace(namespace) + checkErr(err) + + caller := std.GetOrigCaller() + namespaces.Set(namespace, &Space{ + Admins: []std.Address{caller}, + }) +} + +func AddAdmin(namespace string, newAdmin std.Address) { + space := getSpace(namespace) + assertIsAdmin(space) + space.addAdmin(newAdmin) +} + +func RemoveAdmin(namespace string, admin std.Address) { + space := getSpace(namespace) + assertIsAdmin(space) + err := space.removeAdmin(admin) + checkErr(err) +} + +func AddEditor(namespace string, newEditor std.Address) { + space := getSpace(namespace) + assertIsAdmin(space) + space.addEditor(newEditor) +} + +func RemoveEditor(namespace string, editor std.Address) { + space := getSpace(namespace) + assertIsAdmin(space) + err := space.removeEditor(editor) + checkErr(err) +} + +func SetInPause(namespace string, state bool) { + space := getSpace(namespace) + assertIsAdmin(space) + space.InPause = state +} + +// HasPerm returns true if the caller has permission of the namespace. +// If the namespace does not exist, it will return panic. +// If the namespace exists but the caller is not an admin or editor, +// it will return false. +// The vm keeper will use this function to check to add package +func HasPerm(namespace string) bool { + // if enabled is false, it always returns true for dev and testing. + if !enabled { + return true + } + caller := std.GetOrigCaller() + space := getSpace(namespace) + return space.hasPerm(caller) +} + +func Render(path string) string { + // TODO: by address. + + if path == "" { + return renderIndex() + } else if path[:2] == "n/" { + return renderNamespace(path[2:]) + } + return "" +} + +func renderNamespace(namespace string) string { + space := getSpace(namespace) + output := ufmt.Sprintf(` +# %s + +## Admins + +%s + +## Editors + +%s + +## InPause + +%s + +`, namespace, renderAddresses(space.Admins), renderAddresses(space.Editors), formatBool(space.InPause)) + return output +} + +func renderIndex() string { + output := "## Namespaces \n" + namespaces.Iterate("", "", func(namespace string, value interface{}) bool { + space := value.(*Space) + output += ufmt.Sprintf("* [%s](/r/system/names:n/%s) - admins: %d editors: %d inPause: %s \n", + namespace, namespace, len(space.Admins), len(space.Editors), formatBool(space.InPause)) + return false + }) + + return output +} + +func renderAddresses(addresses []std.Address) string { + output := "" + for _, address := range addresses { + output += ufmt.Sprintf("* %s \n", string(address)) + } + return output +} diff --git a/examples/gno.land/r/system/names/public_test.gno b/examples/gno.land/r/system/names/public_test.gno new file mode 100644 index 00000000000..dc840024d2d --- /dev/null +++ b/examples/gno.land/r/system/names/public_test.gno @@ -0,0 +1,74 @@ +package names + +import ( + "std" + "testing" +) + +func TestRegisterNamespace(t *testing.T) { + std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) + creator := std.GetOrigCaller() + + n := "test2" + Register(n) + s := getSpace(n) + assertTrue(t, containsAddress(s.Admins, creator)) +} + +func TestAdminFuncs(t *testing.T) { + std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) + creator := std.GetOrigCaller() + + test1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + test2 := std.Address("g1r6casl322p5adkmmjjkjea404e7f6w29y6gzwg") + + ns := "test3" + + Register(ns) + s := getSpace(ns) + + AddAdmin(ns, test1) + AddAdmin(ns, test2) + assertTrue(t, containsAddress(s.Admins, test1)) + assertTrue(t, containsAddress(s.Admins, test2)) + + RemoveAdmin(ns, test1) + assertFalse(t, containsAddress(s.Admins, test1)) + assertTrue(t, containsAddress(s.Admins, test2)) + + AddEditor(ns, test1) + assertTrue(t, containsAddress(s.Editors, test1)) + + RemoveEditor(ns, test1) + assertTrue(t, !containsAddress(s.Editors, test1)) +} + +func TestSpaceMethods(t *testing.T) { + std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) + creator := std.GetOrigCaller() + + test1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + test2 := std.Address("g1r6casl322p5adkmmjjkjea404e7f6w29y6gzwg") + + ns := "test4" + Register(ns) + s := getSpace(ns) + + assertTrue(t, s.isAdmin(creator)) + assertTrue(t, s.removeAdmin(creator) != nil) + + s.addAdmin(test1) + assertTrue(t, s.removeAdmin(test1) == nil) +} + +func assertTrue(t *testing.T, b bool) { + if !b { + t.Fail() + } +} + +func assertFalse(t *testing.T, b bool) { + if b { + t.Fail() + } +} diff --git a/examples/gno.land/r/system/names/utils.gno b/examples/gno.land/r/system/names/utils.gno new file mode 100644 index 00000000000..5774c1adde5 --- /dev/null +++ b/examples/gno.land/r/system/names/utils.gno @@ -0,0 +1,64 @@ +package names + +import ( + "std" +) + +func getSpace(namespace string) *Space { + s, ok := namespaces.Get(namespace) + if !ok { + panic("namespace does not exist") + } + return s.(*Space) +} + +func existsNamespace(name string) bool { + _, ok := namespaces.Get(name) + return ok +} + +func assertIsAdmin(space *Space) { + caller := std.GetOrigCaller() + if !space.isAdmin(caller) { + panic("Only admins can call this function") + } +} + +func removeAddress(addrs []std.Address, addr std.Address) []std.Address { + var newAddrs []std.Address + for _, a := range addrs { + if a != addr { + newAddrs = append(newAddrs, a) + } + } + return newAddrs +} + +func containsAddress(addrs []std.Address, addr std.Address) bool { + for _, a := range addrs { + if a == addr { + return true + } + } + return false +} + +func isCallerAddress(addr std.Address) bool { + if addr == std.GetOrigCaller() { + return true + } + return false +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} + +func formatBool(b bool) string { + if b { + return "true" + } + return "false" +} diff --git a/tm2/pkg/sdk/vm/keeper.go b/tm2/pkg/sdk/vm/keeper.go index 28531f0a773..f26dafe6134 100644 --- a/tm2/pkg/sdk/vm/keeper.go +++ b/tm2/pkg/sdk/vm/keeper.go @@ -9,6 +9,7 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" @@ -128,6 +129,35 @@ func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { } } +func (vm *VMKeeper) checkNamespacePerm(ctx sdk.Context, creator crypto.Address, pkgPath string) error { + store := vm.getGnoStore(ctx) + + // if r/system/names does not exists -> skip validation. + if pv := store.GetPackage("gno.land/r/system/names", false); pv == nil { + return nil + } + + pathSp := strings.SplitN(pkgPath, "/", 4) // gno.land/r/... + namespace := pathSp[2] + + res, err := vm.Call(ctx, MsgCall{ + Caller: creator, + Send: std.Coins{}, + PkgPath: "gno.land/r/system/names", + Func: "HasPerm", + Args: []string{namespace}, + }) + if err != nil { + return err + } + // TODO: needs fixed representation of bool + if res != "(true bool)" { + return fmt.Errorf("namespace %q not allowed", namespace) + } + + return nil +} + // AddPackage adds a package with given fileset. func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { creator := msg.Creator @@ -153,12 +183,9 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { // Pay deposit from creator. pkgAddr := gno.DerivePkgAddr(pkgPath) - // TODO: ACLs. - // - if r/system/names does not exists -> skip validation. - // - loads r/system/names data state. - // - lookup r/system/names.namespaces for `{r,p}/NAMES`. - // - check if caller is in Admins or Editors. - // - check if namespace is not in pause. + if err := vm.checkNamespacePerm(ctx, creator, pkgPath); err != nil { + return err + } err := vm.bank.SendCoins(ctx, creator, pkgAddr, deposit) if err != nil {