diff --git a/examples/gno.land/p/demo/access/doc.gno b/examples/gno.land/p/demo/access/doc.gno new file mode 100644 index 00000000000..d5d3da89f2e --- /dev/null +++ b/examples/gno.land/p/demo/access/doc.gno @@ -0,0 +1,6 @@ +// Package access provides a simple access control library for managing authorized addresses and controlling resource access. +// +// Features: authorized address management, access verification. +// +// Note: This package is suitable for basic access control in simple use cases. Consider specialized libraries or frameworks for advanced access control and permission management. +package access // import "gno.land/p/demo/access" diff --git a/examples/gno.land/p/demo/access/example_test.gno b/examples/gno.land/p/demo/access/example_test.gno new file mode 100644 index 00000000000..d47083b1252 --- /dev/null +++ b/examples/gno.land/p/demo/access/example_test.gno @@ -0,0 +1,26 @@ +package access + +func ExamplePackageLevel() { + // set this globally or during init() + acl := New() + + // in functions + acl.AssertCurrentHasAccess() +} + +func ExampleEmbedding() { + // declare a new struct, and embed access.Set. + type MyObject struct { + myField int + Set + } + + // initialize the object, it now has its own admin set. + myObject := MyObject{ + myField: 42, + Set: New(), + } + + // check from the context of the object. + myObject.Set.AssertCurrentHasAccess() +} diff --git a/examples/gno.land/p/demo/access/gno.mod b/examples/gno.land/p/demo/access/gno.mod new file mode 100644 index 00000000000..b7752f33dca --- /dev/null +++ b/examples/gno.land/p/demo/access/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/access + +require ( + "gno.land/p/demo/testutils" v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/access/set.gno b/examples/gno.land/p/demo/access/set.gno new file mode 100644 index 00000000000..4d189bf3966 --- /dev/null +++ b/examples/gno.land/p/demo/access/set.gno @@ -0,0 +1,104 @@ +package access + +import "std" + +// New returns a Set object initialized with the caller as the unique authorized address. +// It is recommended to use this function in the `init()` function of the calling realm. +func New() Set { + addr := std.GetOrigCaller() + return Set{ + addrs: []std.Address{addr}, + } +} + +// NewWithAddress returns a Set object initialized with the provided address as authorized. +func NewWithAddress(addr std.Address) Set { + return Set{ + addrs: []std.Address{addr}, + } +} + +// Set is an object containing the configuration and allowing the application of filters. +// It is suited to be used as a contract-side global variable or can be embedded in another Go object. +type Set struct { + addrs []std.Address +} + +// List returns a slice containing all the authorized addresses. +func (s *Set) List() []std.Address { + return s.addrs +} + +// HasAccess checks if the provided address is in the list of authorized ones. +func (s *Set) HasAccess(addr std.Address) bool { + for _, entry := range s.addrs { + if entry == addr { + return true + } + } + return false +} + +// CurrentHasAccess checks if the caller or prevRealm is authorized. +func (s *Set) CurrentHasAccess() bool { + if s.HasAccess(std.GetOrigCaller()) { + return true + } + + // XXX: also check for std.PrevRealm, when merged. + + return false +} + +// AssertCurrentHasAccess checks whether the std.GetOrigCaller or std.PrevRealm is whitelisted as authorized. +// If not, it panics indicating restricted access. +func (s *Set) AssertCurrentHasAccess() { + if !s.CurrentHasAccess() { + panic("restricted access.") + } +} + +// Add adds an address to the list of authorized addresses. +// It requires the caller or prevRealm to be authorized, otherwise, it panics. +func (s *Set) Add(addr std.Address) { + if s.HasAccess(addr) { + // XXX: Consider panicking or handling duplicate addition differently. + return + } + s.addrs = append(s.addrs, addr) +} + +// ForceDel removes an address from the list of authorized addresses. +// It won't panic if there is only one address. +// It requires the caller or prevRealm to be authorized, otherwise, it panics. +func (s *Set) ForceDel(addr std.Address) (success bool) { + for i, entry := range s.addrs { + if entry == addr { + s.addrs = append(s.addrs[:i], s.addrs[i+1:]...) + return true + } + } + return false +} + +// Del removes an address from the list of authorized addresses. +// It requires the caller or prevRealm to be authorized, otherwise, it panics. +func (s *Set) Del(addr std.Address) (success bool) { + if len(s.addrs) == 0 { + panic("should not happen") + } + + // XXX: should we prevent deleting self? + + // Prevent deleting the last authorized address. + if len(s.addrs) == 1 { + panic("cannot have no authorized address.") + } + return s.ForceDel(addr) +} + +// ReplaceAll removes all existing authorized addresses and replaces them with a new one. +// It requires the caller or prevRealm to be authorized, otherwise, it panics. +func (s *Set) Replace(addrs []std.Address) { + s.addrs = addrs +} diff --git a/examples/gno.land/p/demo/access/set_test.gno b/examples/gno.land/p/demo/access/set_test.gno new file mode 100644 index 00000000000..83ba3289df8 --- /dev/null +++ b/examples/gno.land/p/demo/access/set_test.gno @@ -0,0 +1,94 @@ +package access + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestSet_AddAndRemoveAdmin(t *testing.T) { + set := New() + + // Add an admin + admin1 := testutils.TestAddress("test1") + set.Add(admin1) + if !set.HasAccess(admin1) { + t.Errorf("Expected admin1 to be added, but it was not") + } + + // Remove the admin + set.Del(admin1) + if set.HasAccess(admin1) { + t.Errorf("Expected admin1 to be removed, but it was still present") + } +} + +func TestSet_ListAdmins(t *testing.T) { + // Add multiple admins + admin1 := testutils.TestAddress("test2") + admin2 := testutils.TestAddress("test3") + set := NewWithAddress(admin1) + std.TestSetOrigCaller(admin1) + set.Add(admin2) + + // Get the list of admins + admins := set.List() + + // Verify the correct number of admins + expectedAdminsCount := 2 + if len(admins) != expectedAdminsCount { + t.Errorf("Expected %d admins, but got %d", expectedAdminsCount, len(admins)) + } + + // Verify the admins in the list + expectedAdmins := []std.Address{admin1, admin2} + for _, expectedAdmin := range expectedAdmins { + found := false + for _, admin := range admins { + if admin == expectedAdmin { + found = true + break + } + } + if !found { + t.Errorf("Expected admin %s to be in the list, but it was not found", expectedAdmin) + } + } +} + +func TestSet_CurrentHasAccess(t *testing.T) { + // Set the original caller as admin + admin := testutils.TestAddress("test4") + std.TestSetOrigCaller(admin) + set := New() + + // Verify CurrentHasAccess returns true for the admin + if !set.CurrentHasAccess() { + t.Errorf("Expected CurrentHasAccess to return true for the admin, but it returned false") + } + + // Verify CurrentHasAccess returns false for a non-admin + nonAdmin := testutils.TestAddress("test5") + std.TestSetOrigCaller(nonAdmin) + if set.CurrentHasAccess() { + t.Errorf("Expected CurrentHasAccess to return false for a non-admin, but it returned true") + } +} + +func TestSet_AddAndRemoveAdmin_WontPanic(t *testing.T) { + // Add an admin + admin := testutils.TestAddress("test6") + std.TestSetOrigCaller(admin) + set := New() + + // Set a non-admin caller + nonAdmin := testutils.TestAddress("test7") + std.TestSetOrigCaller(nonAdmin) + + // Verify Add won't panic in Privileged mode + set.Add(nonAdmin) + + // Verify Del won't panic in Privileged mode + set.Del(nonAdmin) +} diff --git a/examples/gno.land/p/demo/access/unprivileged.gno b/examples/gno.land/p/demo/access/unprivileged.gno new file mode 100644 index 00000000000..ac0c5bfdd8b --- /dev/null +++ b/examples/gno.land/p/demo/access/unprivileged.gno @@ -0,0 +1,33 @@ +package access + +import "std" + +type UnprivilegedSet struct { + Set +} + +// Unprivileged returns an unprivileged structure that can be securely exposed as object. +func (s Set) Unprivileged() UnprivilegedSet { + return UnprivilegedSet{Set: s} +} + +func (u *UnprivilegedSet) Add(addr std.Address) { + u.AssertCurrentHasAccess() + u.Set.Add(addr) +} + +func (u *UnprivilegedSet) Del(addr std.Address) { + u.AssertCurrentHasAccess() + u.Set.Del(addr) +} + +func (u *UnprivilegedSet) ForceDel(addr std.Address) { + u.AssertCurrentHasAccess() + u.Set.ForceDel(addr) +} + +func (u *UnprivilegedSet) Replace(addr std.Address) { + u.AssertCurrentHasAccess() + addrs := []std.Address{addr} + u.Set.Replace(addrs) +} diff --git a/examples/gno.land/p/demo/access/unprivileged_test.gno b/examples/gno.land/p/demo/access/unprivileged_test.gno new file mode 100644 index 00000000000..7d36143f6d2 --- /dev/null +++ b/examples/gno.land/p/demo/access/unprivileged_test.gno @@ -0,0 +1,104 @@ +package access + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestUnprivilegedSet_AddAndRemoveAdmin(t *testing.T) { + set := New().Unprivileged() + + // Add an admin + admin1 := testutils.TestAddress("test1") + set.Add(admin1) + if !set.HasAccess(admin1) { + t.Errorf("Expected admin1 to be added, but it was not") + } + + // Remove the admin + set.Del(admin1) + if set.HasAccess(admin1) { + t.Errorf("Expected admin1 to be removed, but it was still present") + } +} + +func TestUnprivilegedSet_ListAdmins(t *testing.T) { + // Add multiple admins + admin1 := testutils.TestAddress("test2") + admin2 := testutils.TestAddress("test3") + set := NewWithAddress(admin1).Unprivileged() + std.TestSetOrigCaller(admin1) + set.Add(admin2) + + // Get the list of admins + admins := set.List() + + // Verify the correct number of admins + expectedAdminsCount := 2 + if len(admins) != expectedAdminsCount { + t.Errorf("Expected %d admins, but got %d", expectedAdminsCount, len(admins)) + } + + // Verify the admins in the list + expectedAdmins := []std.Address{admin1, admin2} + for _, expectedAdmin := range expectedAdmins { + found := false + for _, admin := range admins { + if admin == expectedAdmin { + found = true + break + } + } + if !found { + t.Errorf("Expected admin %s to be in the list, but it was not found", expectedAdmin) + } + } +} + +func TestUnprivilegedSet_CurrentHasAccess(t *testing.T) { + // Set the original caller as admin + admin := testutils.TestAddress("test4") + std.TestSetOrigCaller(admin) + set := New().Unprivileged() + + // Verify CurrentHasAccess returns true for the admin + if !set.CurrentHasAccess() { + t.Errorf("Expected CurrentHasAccess to return true for the admin, but it returned false") + } + + // Verify CurrentHasAccess returns false for a non-admin + nonAdmin := testutils.TestAddress("test5") + std.TestSetOrigCaller(nonAdmin) + if set.CurrentHasAccess() { + t.Errorf("Expected CurrentHasAccess to return false for a non-admin, but it returned true") + } +} + +func TestUnprivilegedSet_AddAndRemoveAdmin_Panic(t *testing.T) { + // Add an admin + admin := testutils.TestAddress("test6") + std.TestSetOrigCaller(admin) + set := New().Unprivileged() + + // Set a non-admin caller + nonAdmin := testutils.TestAddress("test7") + std.TestSetOrigCaller(nonAdmin) + + // Verify Add panics when called by a non-admin + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected Add to panic when called by a non-admin, but it did not panic") + } + }() + set.Add(admin) + + // Verify Del panics when called by a non-admin + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected Del to panic when called by a non-admin, but it did not panic") + } + }() + set.Del(admin) +} diff --git a/examples/gno.land/p/demo/access/z0_filetest.gno b/examples/gno.land/p/demo/access/z0_filetest.gno new file mode 100644 index 00000000000..45a0f9a39a6 --- /dev/null +++ b/examples/gno.land/p/demo/access/z0_filetest.gno @@ -0,0 +1,44 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "std" + + "gno.land/p/demo/access" + "gno.land/p/demo/testutils" +) + +var admins access.Set + +func init() { + admins = access.New() +} + +func main() { + initialCaller := std.GetOrigCaller() + test1 := testutils.TestAddress("test1") + test2 := testutils.TestAddress("test2") + println(admins.List()) + admins.Add(test1) + println(admins.List()) + admins.Add(test1) + println(admins.List()) + println(admins.CurrentHasAccess()) + admins.Del(initialCaller) + println(admins.List()) + println(admins.CurrentHasAccess()) + std.TestSetOrigCaller(test2) + println(admins.CurrentHasAccess()) + std.TestSetOrigCaller(test1) + println(admins.CurrentHasAccess()) +} + +// Output: +// slice[ref(a8ada09dee16d791fd406d629fe29bb0ed084a30:5)] +// slice[("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" std.Address),("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// slice[("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" std.Address),("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// true +// slice[("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// false +// false +// true diff --git a/examples/gno.land/p/demo/access/z1_filetest.gno b/examples/gno.land/p/demo/access/z1_filetest.gno new file mode 100644 index 00000000000..4d2fc12b056 --- /dev/null +++ b/examples/gno.land/p/demo/access/z1_filetest.gno @@ -0,0 +1,40 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "std" + + "gno.land/p/demo/access" + "gno.land/p/demo/testutils" +) + +var Admins = access.New().Unprivileged() + +func main() { + initialCaller := std.GetOrigCaller() + test1 := testutils.TestAddress("test1") + test2 := testutils.TestAddress("test2") + println(Admins.List()) + Admins.Add(test1) + println(Admins.List()) + Admins.Add(test1) + println(Admins.List()) + println(Admins.CurrentHasAccess()) + Admins.Del(initialCaller) + println(Admins.List()) + println(Admins.CurrentHasAccess()) + std.TestSetOrigCaller(test2) + println(Admins.CurrentHasAccess()) + std.TestSetOrigCaller(test1) + println(Admins.CurrentHasAccess()) +} + +// Output: +// slice[ref(a8ada09dee16d791fd406d629fe29bb0ed084a30:6)] +// slice[("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" std.Address),("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// slice[("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" std.Address),("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// true +// slice[("g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7" std.Address)] +// false +// false +// true diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 3becb7022dd..ab50938a3e0 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -4,39 +4,44 @@ import ( "std" "strings" - "gno.land/p/demo/avl" + "gno.land/p/demo/access" ) var ( - adminAddr std.Address - moderatorList avl.Tree - commenterList avl.Tree - inPause bool + admins access.Set + moderators access.Set + commenters access.Set + inPause bool ) func init() { - // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + admins = access.New() + admins.Add("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred } -func AdminSetAdminAddr(addr std.Address) { +func AdminSetInPause(state bool) { assertIsAdmin() - adminAddr = addr + inPause = state } -func AdminSetInPause(state bool) { +func AdminAddAdmin(addr std.Address) { assertIsAdmin() - inPause = state + admins.Add(addr) +} + +func AdminDelAdmin(addr std.Address) { + assertIsAdmin() + admins.Del(addr) } func AdminAddModerator(addr std.Address) { assertIsAdmin() - moderatorList.Set(addr.String(), true) + moderators.Add(addr) } -func AdminRemoveModerator(addr std.Address) { +func AdminDelModerator(addr std.Address) { assertIsAdmin() - moderatorList.Set(addr.String(), false) // FIXME: delete instead? + moderators.Del(addr) } func ModAddPost(slug, title, body, publicationDate, authors, tags string) { @@ -76,12 +81,12 @@ func ModRemovePost(slug string) { func ModAddCommenter(addr std.Address) { assertIsModerator() - commenterList.Set(addr.String(), true) + commenters.Add(addr) } func ModDelCommenter(addr std.Address) { assertIsModerator() - commenterList.Set(addr.String(), false) // FIXME: delete instead? + commenters.Del(addr) } func ModDelComment(slug string, index int) { @@ -91,38 +96,19 @@ func ModDelComment(slug string, index int) { checkErr(err) } -func isAdmin(addr std.Address) bool { - return addr == adminAddr -} - -func isModerator(addr std.Address) bool { - _, found := moderatorList.Get(addr.String()) - return found -} - -func isCommenter(addr std.Address) bool { - _, found := commenterList.Get(addr.String()) - return found -} - func assertIsAdmin() { - caller := std.GetOrigCaller() - if !isAdmin(caller) { - panic("access restricted.") - } + admins.AssertCurrentHasAccess() } func assertIsModerator() { - caller := std.GetOrigCaller() - if isAdmin(caller) || isModerator(caller) { + if admins.CurrentHasAccess() || moderators.CurrentHasAccess() { return } panic("access restricted") } func assertIsCommenter() { - caller := std.GetOrigCaller() - if isAdmin(caller) || isModerator(caller) || isCommenter(caller) { + if admins.CurrentHasAccess() || moderators.CurrentHasAccess() || commenters.CurrentHasAccess() { return } panic("access restricted") diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 1d64238cdc8..7d36d330a2d 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -1,6 +1,7 @@ module gno.land/r/gnoland/blog require ( + gno.land/p/demo/access v0.0.0-latest gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest )