diff --git a/examples/gno.land/p/demo/nestedpkg/gno.mod b/examples/gno.land/p/demo/nestedpkg/gno.mod new file mode 100644 index 00000000000..b6071128815 --- /dev/null +++ b/examples/gno.land/p/demo/nestedpkg/gno.mod @@ -0,0 +1,3 @@ +module nestedpkg + +require gno.land/r/demo/tests v0.0.0-latest diff --git a/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno b/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno new file mode 100644 index 00000000000..cf44dae007b --- /dev/null +++ b/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno @@ -0,0 +1,84 @@ +// Package nestedpkg provides helpers for package-path based access control. +// It is useful for upgrade patterns relying on namespaces. +package nestedpkg + +import ( + "std" + "strings" +) + +// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm. +func IsCallerSubPath() bool { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + return strings.HasPrefix(prev, cur) +} + +// AssertCallerIsSubPath panics if IsCallerSubPath returns false. +func AssertCallerIsSubPath() { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + if !strings.HasPrefix(prev, cur) { + panic("call restricted to nested packages. current realm is " + cur + ", previous realm is " + prev) + } +} + +// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm. +func IsCallerParentPath() bool { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + return strings.HasPrefix(cur, prev) +} + +// AssertCallerIsParentPath panics if IsCallerParentPath returns false. +func AssertCallerIsParentPath() { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + if !strings.HasPrefix(cur, prev) { + panic("call restricted to parent packages. current realm is " + cur + ", previous realm is " + prev) + } +} + +// IsSameNamespace checks if the caller realm and the current realm are in the same namespace. +func IsSameNamespace() bool { + var ( + cur = nsFromPath(std.CurrentRealm().PkgPath()) + "/" + prev = nsFromPath(std.PrevRealm().PkgPath()) + "/" + ) + return cur == prev +} + +// AssertIsSameNamespace panics if IsSameNamespace returns false. +func AssertIsSameNamespace() { + var ( + cur = nsFromPath(std.CurrentRealm().PkgPath()) + "/" + prev = nsFromPath(std.PrevRealm().PkgPath()) + "/" + ) + if cur != prev { + panic("call restricted to packages from the same namespace. current realm is " + cur + ", previous realm is " + prev) + } +} + +// nsFromPath extracts the namespace from a package path. +func nsFromPath(pkgpath string) string { + parts := strings.Split(pkgpath, "/") + + // Specifically for gno.land, potential paths are in the form of DOMAIN/r/NAMESPACE/... + // XXX: Consider extra checks. + // XXX: Support non gno.land domains, where p/ and r/ won't be enforced. + if len(parts) >= 3 { + return parts[2] + } + return "" +} + +// XXX: Consider adding IsCallerDirectlySubPath +// XXX: Consider adding IsCallerDirectlyParentPath diff --git a/examples/gno.land/p/demo/nestedpkg/nestedpkg_test.gno b/examples/gno.land/p/demo/nestedpkg/nestedpkg_test.gno new file mode 100644 index 00000000000..bc0a98f47fb --- /dev/null +++ b/examples/gno.land/p/demo/nestedpkg/nestedpkg_test.gno @@ -0,0 +1,75 @@ +package nestedpkg + +import ( + "std" + "testing" + + "gno.land/r/demo/tests" +) + +func TestPackage(t *testing.T) { + // direct child + cur := "gno.land/r/demo/tests/foo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if !tests.IsCallerSubPath() { + t.Errorf(cur + " should be a sub path") + } + if tests.IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if !tests.HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // grand-grand-child + cur = "gno.land/r/demo/tests/foo/bar/baz" + std.TestSetRealm(std.NewCodeRealm(cur)) + if !tests.IsCallerSubPath() { + t.Errorf(cur + " should be a sub path") + } + if tests.IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if !tests.HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // direct parent + cur = "gno.land/r/demo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if tests.IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if !tests.IsCallerParentPath() { + t.Errorf(cur + " should be a parent path") + } + if !tests.HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // fake parent (prefix) + cur = "gno.land/r/dem" + std.TestSetRealm(std.NewCodeRealm(cur)) + if tests.IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if tests.IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if tests.HasCallerSameNamespace() { + t.Errorf(cur + " should not be from the same namespace") + } + + // different namespace + cur = "gno.land/r/foo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if tests.IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if tests.IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if tests.HasCallerSameNamespace() { + t.Errorf(cur + " should not be from the same namespace") + } +} diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index f34d41d327a..c51571e7d04 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/demo/tests -require gno.land/r/demo/tests/subtests v0.0.0-latest +require ( + gno.land/p/demo/nestedpkg v0.0.0-latest + gno.land/r/demo/tests/subtests v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 773412c3db9..421ac6528c9 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -3,6 +3,7 @@ package tests import ( "std" + "gno.land/p/demo/nestedpkg" rsubtests "gno.land/r/demo/tests/subtests" ) @@ -99,3 +100,15 @@ func GetRSubtestsPrevRealm() std.Realm { func Exec(fn func()) { fn() } + +func IsCallerSubPath() bool { + return nestedpkg.IsCallerSubPath() +} + +func IsCallerParentPath() bool { + return nestedpkg.IsCallerParentPath() +} + +func HasCallerSameNamespace() bool { + return nestedpkg.IsSameNamespace() +}