Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add p/demo/access (administrable contracts) #880

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/access/doc.gno
Original file line number Diff line number Diff line change
@@ -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"
26 changes: 26 additions & 0 deletions examples/gno.land/p/demo/access/example_test.gno
Original file line number Diff line number Diff line change
@@ -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()
}
5 changes: 5 additions & 0 deletions examples/gno.land/p/demo/access/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module gno.land/p/demo/access

require (
"gno.land/p/demo/testutils" v0.0.0-latest
)
104 changes: 104 additions & 0 deletions examples/gno.land/p/demo/access/set.gno
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a better naming? Set is a very generic name.

Here's few suggestions:

AuthorizationSet: Describes that it holds a list of authorized addresses.
AuthSet: Short for "Authorization Set", indicating its purpose of holding authorized addresses.
AccessControl: Suggests that it is used for controlling access to certain resources.
WhiteList: Suggests that it acts as a whitelist for permitted addresses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe?

-type Set struct {
-	addrs []std.Address
-}
+type AuthSet []std.Address

Since this struct only contains one field

Copy link
Contributor

@harry-hov harry-hov Jul 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about having something like this

type AuthSet struct {
	entries []AuthSetEntry
}

type AuthSetEntry struct {
	role    Role // `Admin` etc
	address std.Address
}

It can provide better access control.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected usage is: access.Set which IMO makes sense for such specialized pure packages.

However, I agree with you in other cases, especially apps or just big libraries.

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
}
Comment on lines +64 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a specific method to check duplicates, or a different data structure instead of using a method that is doing the same now, but it might not in the future.

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 {
moul marked this conversation as resolved.
Show resolved Hide resolved
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
}
94 changes: 94 additions & 0 deletions examples/gno.land/p/demo/access/set_test.gno
Original file line number Diff line number Diff line change
@@ -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)
}
33 changes: 33 additions & 0 deletions examples/gno.land/p/demo/access/unprivileged.gno
Original file line number Diff line number Diff line change
@@ -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)
}
104 changes: 104 additions & 0 deletions examples/gno.land/p/demo/access/unprivileged_test.gno
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading