From d1f0fce47791e5416a0e9ed117f1abc64817f577 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 25 Dec 2024 20:03:28 +0100 Subject: [PATCH 01/38] feat: auth pattern Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 16 +++++ examples/gno.land/p/demo/auth/gno.mod | 1 + .../gno.land/r/demo/authbanker/authbanker.gno | 62 +++++++++++++++++++ examples/gno.land/r/demo/authbanker/gno.mod | 1 + examples/gno.land/r/demo/authreg/authreg.gno | 26 ++++++++ examples/gno.land/r/demo/authreg/gno.mod | 1 + examples/gno.land/r/demo/subacc/gno.mod | 1 + examples/gno.land/r/demo/subacc/subacc.gno | 47 ++++++++++++++ .../gno.land/r/demo/subacc/subacc_test.gno | 26 ++++++++ 9 files changed, 181 insertions(+) create mode 100644 examples/gno.land/p/demo/auth/auth.gno create mode 100644 examples/gno.land/p/demo/auth/gno.mod create mode 100644 examples/gno.land/r/demo/authbanker/authbanker.gno create mode 100644 examples/gno.land/r/demo/authbanker/gno.mod create mode 100644 examples/gno.land/r/demo/authreg/authreg.gno create mode 100644 examples/gno.land/r/demo/authreg/gno.mod create mode 100644 examples/gno.land/r/demo/subacc/gno.mod create mode 100644 examples/gno.land/r/demo/subacc/subacc.gno create mode 100644 examples/gno.land/r/demo/subacc/subacc_test.gno diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno new file mode 100644 index 00000000000..3be76cfb19e --- /dev/null +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -0,0 +1,16 @@ +package auth + +import ( + "errors" + "std" +) + +type Token interface { + Source() std.Realm +} + +type AuthenticateFn = func(auth Token) string + +var ( + ErrInvalidToken = errors.New("invalid token") +) diff --git a/examples/gno.land/p/demo/auth/gno.mod b/examples/gno.land/p/demo/auth/gno.mod new file mode 100644 index 00000000000..3eada3a47d0 --- /dev/null +++ b/examples/gno.land/p/demo/auth/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/auth \ No newline at end of file diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno new file mode 100644 index 00000000000..942f44d7650 --- /dev/null +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -0,0 +1,62 @@ +package authbanker + +import ( + "std" + "strings" + + "gno.land/p/demo/auth" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/authreg" +) + +// this example is there mostly to demonstrate auth usage, it is quite limited +// only EOAs can fund accounts + +const denom = "ugnot" + +var vaults = make(map[string]int64) + +func GetCoins(account string) int64 { + return vaults[account] +} + +func SendCoins(atok auth.Token, to string, amount int64) { + account := authreg.Authenticate(atok) + + if amount < 1 { + panic(ufmt.Errorf("sent amount of %q must be >= 0", denom)) + } + if vaultAmount := vaults[account]; amount > vaultAmount { + panic(ufmt.Errorf("not enough %q in account, wanted %d, got %d", denom, amount, vaultAmount)) + } + + vaults[account] -= amount + + if strings.HasPrefix(to, "/") { + vaults[to] += amount + } else { + realmBanker := std.GetBanker(std.BankerTypeRealmSend) + from := std.CurrentRealm().Addr() + coins := std.Coins{std.NewCoin(denom, amount)} + realmBanker.SendCoins(from, std.Address(to), coins) + } +} + +func FundVault(to string) { + if !strings.HasPrefix(to, "/") { + panic("invalid destination") + } + // XXX: maybe `authreg.Validate(to)` + std.AssertOriginCall() + sentCoins := std.GetOrigSend() + for _, coin := range sentCoins { + if coin.Denom != denom { + panic(ufmt.Errorf("only %q supported", denom)) + } + vaults[to] += coin.Amount + } +} + +func TotalCoin(denom string) int64 { + return std.GetBanker(std.BankerTypeRealmSend).TotalCoin(denom) +} diff --git a/examples/gno.land/r/demo/authbanker/gno.mod b/examples/gno.land/r/demo/authbanker/gno.mod new file mode 100644 index 00000000000..d76938eb808 --- /dev/null +++ b/examples/gno.land/r/demo/authbanker/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/authbanker \ No newline at end of file diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno new file mode 100644 index 00000000000..bf7407d885b --- /dev/null +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -0,0 +1,26 @@ +package authreg + +import ( + "errors" + "path" + "std" + + "gno.land/p/demo/auth" +) + +var fns = make(map[string]auth.AuthenticateFn) + +// XXX: we could add a slug there +func Register(authenticate auth.AuthenticateFn) { + caller := std.PrevRealm().Addr() + fns[caller.String()] = authenticate +} + +func Authenticate(autok auth.Token) string { + provider := autok.Source().Addr().String() + authFn, ok := fns[provider] + if !ok { + panic(errors.New("unknown auth provider")) + } + return path.Join("/", provider, authFn(autok)) +} diff --git a/examples/gno.land/r/demo/authreg/gno.mod b/examples/gno.land/r/demo/authreg/gno.mod new file mode 100644 index 00000000000..2818c069540 --- /dev/null +++ b/examples/gno.land/r/demo/authreg/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/authreg \ No newline at end of file diff --git a/examples/gno.land/r/demo/subacc/gno.mod b/examples/gno.land/r/demo/subacc/gno.mod new file mode 100644 index 00000000000..5d5183885cc --- /dev/null +++ b/examples/gno.land/r/demo/subacc/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/subacc \ No newline at end of file diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno new file mode 100644 index 00000000000..b7a85fb128a --- /dev/null +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -0,0 +1,47 @@ +package subacc + +import ( + "path" + "std" + + "gno.land/p/demo/auth" + "gno.land/r/demo/authreg" +) + +var source std.Realm + +func init() { + source = std.CurrentRealm() + authreg.Register(authenticate) +} + +func AuthToken(slug string) auth.Token { + caller := std.PrevRealm().Addr() + return &token{accountKey: accountKey(caller, slug)} +} + +type token struct { + accountKey string +} + +func (a *token) Source() std.Realm { + return source +} + +var _ auth.Token = (*token)(nil) + +func authenticate(autho auth.Token) string { + // this check should ensure we created this object + cauth, ok := autho.(*token) + if !ok { + panic(auth.ErrInvalidToken) + } + + return cauth.accountKey +} + +var _ auth.AuthenticateFn = authenticate + +func accountKey(creator std.Address, slug string) string { + return path.Join("/", creator.String(), slug) +} diff --git a/examples/gno.land/r/demo/subacc/subacc_test.gno b/examples/gno.land/r/demo/subacc/subacc_test.gno new file mode 100644 index 00000000000..e0ec474e89c --- /dev/null +++ b/examples/gno.land/r/demo/subacc/subacc_test.gno @@ -0,0 +1,26 @@ +package subacc_test + +import ( + "path" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/authbanker" +) + +var ( + alice = testutils.TestAddress("alice") +) + +func TestSubacc(t *testing.T) { + accountId := path.Join("/", std.DerivePkgAddr("gno.land/r/demo/subacc").String(), alice.String(), "foo") + + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil) + authbanker.FundVault(accountId) + + println(authbanker.GetCoins(accountId)) + + // XXX: ./examples/gno.land/r/demo/subacc: test pkg: panic: unexpected unreal object +} From 8ba0626a27594e3bc941a018f1df6b0482e7d078 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 10:54:38 +0100 Subject: [PATCH 02/38] chore: mod tidy Signed-off-by: Norman --- examples/gno.land/p/demo/auth/gno.mod | 2 +- examples/gno.land/r/demo/authbanker/gno.mod | 2 +- examples/gno.land/r/demo/authreg/gno.mod | 2 +- examples/gno.land/r/demo/subacc/gno.mod | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/demo/auth/gno.mod b/examples/gno.land/p/demo/auth/gno.mod index 3eada3a47d0..056ab54eab3 100644 --- a/examples/gno.land/p/demo/auth/gno.mod +++ b/examples/gno.land/p/demo/auth/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/auth \ No newline at end of file +module gno.land/p/demo/auth diff --git a/examples/gno.land/r/demo/authbanker/gno.mod b/examples/gno.land/r/demo/authbanker/gno.mod index d76938eb808..f687a9f3477 100644 --- a/examples/gno.land/r/demo/authbanker/gno.mod +++ b/examples/gno.land/r/demo/authbanker/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/authbanker \ No newline at end of file +module gno.land/p/demo/authbanker diff --git a/examples/gno.land/r/demo/authreg/gno.mod b/examples/gno.land/r/demo/authreg/gno.mod index 2818c069540..3a069e51d98 100644 --- a/examples/gno.land/r/demo/authreg/gno.mod +++ b/examples/gno.land/r/demo/authreg/gno.mod @@ -1 +1 @@ -module gno.land/r/demo/authreg \ No newline at end of file +module gno.land/r/demo/authreg diff --git a/examples/gno.land/r/demo/subacc/gno.mod b/examples/gno.land/r/demo/subacc/gno.mod index 5d5183885cc..8d5d24a0cbf 100644 --- a/examples/gno.land/r/demo/subacc/gno.mod +++ b/examples/gno.land/r/demo/subacc/gno.mod @@ -1 +1 @@ -module gno.land/r/demo/subacc \ No newline at end of file +module gno.land/r/demo/subacc From f0b169bfec97ac0d70b1bddc00331553a92006ca Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:12:26 +0100 Subject: [PATCH 03/38] chore: add core auth test Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 2 +- examples/gno.land/p/demo/auth/auth_test.gno | 60 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 examples/gno.land/p/demo/auth/auth_test.gno diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index 3be76cfb19e..21091d788b6 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -6,7 +6,7 @@ import ( ) type Token interface { - Source() std.Realm + Source() std.Realm // this can be spoofed and is only useful for registries } type AuthenticateFn = func(auth Token) string diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno new file mode 100644 index 00000000000..6cf374d1aae --- /dev/null +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -0,0 +1,60 @@ +package auth_test + +import ( + "std" + "testing" + + "gno.land/p/demo/auth" + "gno.land/p/demo/urequire" +) + +func TestValidToken(t *testing.T) { + urequire.NotPanics(t, func() { + authenticate(getToken()) + }) +} + +func TestFakeToken(t *testing.T) { + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + authenticate(getFakeToken()) + }) +} + +const testKey = "valid" + +type token struct { +} + +func (t *token) Source() std.Realm { + return std.CurrentRealm() +} + +var _ auth.Token = (*token)(nil) + +func getToken() auth.Token { + return &token{} +} + +func authenticate(autok auth.Token) string { + // the next line is the core of the auth pattern, this ensures we created this token + _, ok := autok.(*token) + if !ok { + panic(auth.ErrInvalidToken) + } + return testKey +} + +var _ auth.AuthenticateFn = authenticate + +type fakeToken struct { +} + +func (t *fakeToken) Source() std.Realm { + return std.CurrentRealm() +} + +var _ auth.Token = (*fakeToken)(nil) + +func getFakeToken() auth.Token { + return &fakeToken{} +} From 095aa6a1370ab467678ca78f585c02e703279784 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:15:31 +0100 Subject: [PATCH 04/38] chore: simplify test and add doc Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 1 + examples/gno.land/p/demo/auth/auth_test.gno | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index 21091d788b6..786727fffd2 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -9,6 +9,7 @@ type Token interface { Source() std.Realm // this can be spoofed and is only useful for registries } +// AuthenticateFn validates a token and returns the ID of the authenticated entity type AuthenticateFn = func(auth Token) string var ( diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index 6cf374d1aae..bff4305fb13 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -20,8 +20,6 @@ func TestFakeToken(t *testing.T) { }) } -const testKey = "valid" - type token struct { } @@ -41,7 +39,7 @@ func authenticate(autok auth.Token) string { if !ok { panic(auth.ErrInvalidToken) } - return testKey + return "" } var _ auth.AuthenticateFn = authenticate From ea3e5dfe82a755beafbb9daf0f01a936064d0412 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:21:57 +0100 Subject: [PATCH 05/38] chore: more doc Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index 786727fffd2..d80144be1b3 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -5,11 +5,18 @@ import ( "std" ) +// Token represents an authentication token type Token interface { - Source() std.Realm // this can be spoofed and is only useful for registries + // Source is used to identify the provenance of the token. + // It is intented to be used to find the corresponding [AuthenticateFn], for example in registries. + // It can be spoofed and should not be trusted. + Source() std.Realm } -// AuthenticateFn validates a token and returns the ID of the authenticated entity +// AuthenticateFn validates a token and returns the ID of the authenticated entity. +// It panics with [ErrInvalidToken] if the token is invalid. +// +// XXX: Should we add some kind of `Token.EntityID` method instead of returning the ID here? type AuthenticateFn = func(auth Token) string var ( From 39a78aefa7931ccc24e4b48d0ba808963784e913 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:25:09 +0100 Subject: [PATCH 06/38] chore: use test realm Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index bff4305fb13..b4a27fc984c 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -20,11 +20,13 @@ func TestFakeToken(t *testing.T) { }) } +var testRealm = std.NewCodeRealm("gno.land/r/demo/absacc") + type token struct { } func (t *token) Source() std.Realm { - return std.CurrentRealm() + return testRealm } var _ auth.Token = (*token)(nil) @@ -48,7 +50,7 @@ type fakeToken struct { } func (t *fakeToken) Source() std.Realm { - return std.CurrentRealm() + return testRealm } var _ auth.Token = (*fakeToken)(nil) From f564760c8792027d7a5341db6d96a66107aaf3fe Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:29:59 +0100 Subject: [PATCH 07/38] chore: more explicit example in test Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index b4a27fc984c..0542599421e 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -41,7 +41,7 @@ func authenticate(autok auth.Token) string { if !ok { panic(auth.ErrInvalidToken) } - return "" + return "alice" } var _ auth.AuthenticateFn = authenticate From 322bfa769506ed05458e13c63e9b91e88ac38607 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 11:47:31 +0100 Subject: [PATCH 08/38] chore: rename arg Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index d80144be1b3..7a3fb8b687b 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -17,7 +17,7 @@ type Token interface { // It panics with [ErrInvalidToken] if the token is invalid. // // XXX: Should we add some kind of `Token.EntityID` method instead of returning the ID here? -type AuthenticateFn = func(auth Token) string +type AuthenticateFn = func(autok Token) string var ( ErrInvalidToken = errors.New("invalid token") From c479c9cded18c90507c8b51cc1becf1d499d77b5 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 12:27:55 +0100 Subject: [PATCH 09/38] chore: improve integration test and expose subacc.EntityID Signed-off-by: Norman --- .../gno.land/r/demo/authbanker/authbanker.gno | 3 ++ .../r/demo/subacc/integration_test.gno | 45 +++++++++++++++++++ examples/gno.land/r/demo/subacc/subacc.gno | 10 +++-- .../gno.land/r/demo/subacc/subacc_test.gno | 26 ----------- 4 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 examples/gno.land/r/demo/subacc/integration_test.gno delete mode 100644 examples/gno.land/r/demo/subacc/subacc_test.gno diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index 942f44d7650..c0ac6d9e66f 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -29,6 +29,9 @@ func SendCoins(atok auth.Token, to string, amount int64) { if vaultAmount := vaults[account]; amount > vaultAmount { panic(ufmt.Errorf("not enough %q in account, wanted %d, got %d", denom, amount, vaultAmount)) } + if account == to { + panic("cannot send to self") + } vaults[account] -= amount diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/subacc/integration_test.gno new file mode 100644 index 00000000000..bfcf0713408 --- /dev/null +++ b/examples/gno.land/r/demo/subacc/integration_test.gno @@ -0,0 +1,45 @@ +package subacc_test + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" + "gno.land/r/demo/authbanker" + "gno.land/r/demo/subacc" +) + +func TestEntityID(t *testing.T) { + alice := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + expected := "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/savings" + urequire.Equal(t, expected, subacc.EntityID(alice, "savings")) +} + +func TestSubacc(t *testing.T) { + alice := testutils.TestAddress("alice") + aliceAccountID := subacc.EntityID(alice, "savings") + + bob := testutils.TestAddress("bob") + bobAccountID := subacc.EntityID(bob, "savings") + + std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil) + authbanker.FundVault(aliceAccountID) + urequire.Equal(t, int64(42), authbanker.GetCoins(aliceAccountID)) + + std.TestSetRealm(std.NewUserRealm(alice)) + aliceToken := subacc.AuthToken("savings") + authbanker.SendCoins(aliceToken, bobAccountID, 14) + urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) + urequire.Equal(t, int64(14), authbanker.GetCoins(bobAccountID)) + + std.TestSetRealm(std.NewUserRealm(bob)) + bobToken := subacc.AuthToken("savings") + urequire.PanicsWithMessage(t, `not enough "ugnot" in account, wanted 15, got 14`, func() { authbanker.SendCoins(bobToken, alice.String(), 15) }) + /* + authbanker.SendCoins(bobToken, alice.String(), 7) + urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) + urequire.Equal(t, int64(7), authbanker.GetCoins(bobAccountID)) + urequire.Equal(t, int64(7), std.GetBanker(std.BankerTypeReadonly).GetCoins(alice)[0].Amount) + */ +} diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno index b7a85fb128a..da6f1bb396c 100644 --- a/examples/gno.land/r/demo/subacc/subacc.gno +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -17,7 +17,7 @@ func init() { func AuthToken(slug string) auth.Token { caller := std.PrevRealm().Addr() - return &token{accountKey: accountKey(caller, slug)} + return &token{accountKey: accountID(caller, slug)} } type token struct { @@ -42,6 +42,10 @@ func authenticate(autho auth.Token) string { var _ auth.AuthenticateFn = authenticate -func accountKey(creator std.Address, slug string) string { - return path.Join("/", creator.String(), slug) +func EntityID(creator std.Address, slug string) string { + return path.Join("/", std.CurrentRealm().Addr().String(), accountID(creator, slug)) +} + +func accountID(creator std.Address, slug string) string { + return creator.String() + "/" + slug } diff --git a/examples/gno.land/r/demo/subacc/subacc_test.gno b/examples/gno.land/r/demo/subacc/subacc_test.gno deleted file mode 100644 index e0ec474e89c..00000000000 --- a/examples/gno.land/r/demo/subacc/subacc_test.gno +++ /dev/null @@ -1,26 +0,0 @@ -package subacc_test - -import ( - "path" - "std" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/r/demo/authbanker" -) - -var ( - alice = testutils.TestAddress("alice") -) - -func TestSubacc(t *testing.T) { - accountId := path.Join("/", std.DerivePkgAddr("gno.land/r/demo/subacc").String(), alice.String(), "foo") - - std.TestSetRealm(std.NewUserRealm(alice)) - std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil) - authbanker.FundVault(accountId) - - println(authbanker.GetCoins(accountId)) - - // XXX: ./examples/gno.land/r/demo/subacc: test pkg: panic: unexpected unreal object -} From 60fc391e0c369fa2b9abcb807563adf9219cec3e Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:00:45 +0100 Subject: [PATCH 10/38] chore: improve integration test Signed-off-by: Norman --- .../gno.land/r/demo/authbanker/authbanker.gno | 8 ++----- .../r/demo/subacc/integration_test.gno | 23 ++++++++----------- examples/gno.land/r/demo/subacc/subacc.gno | 2 +- .../gno.land/r/demo/subacc/subacc_test.gno | 15 ++++++++++++ 4 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 examples/gno.land/r/demo/subacc/subacc_test.gno diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index c0ac6d9e66f..47a6455a271 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -24,10 +24,10 @@ func SendCoins(atok auth.Token, to string, amount int64) { account := authreg.Authenticate(atok) if amount < 1 { - panic(ufmt.Errorf("sent amount of %q must be >= 0", denom)) + panic("sent amount must be >= 0") } if vaultAmount := vaults[account]; amount > vaultAmount { - panic(ufmt.Errorf("not enough %q in account, wanted %d, got %d", denom, amount, vaultAmount)) + panic("not enough in account") } if account == to { panic("cannot send to self") @@ -59,7 +59,3 @@ func FundVault(to string) { vaults[to] += coin.Amount } } - -func TotalCoin(denom string) int64 { - return std.GetBanker(std.BankerTypeRealmSend).TotalCoin(denom) -} diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/subacc/integration_test.gno index bfcf0713408..c122fc7ee56 100644 --- a/examples/gno.land/r/demo/subacc/integration_test.gno +++ b/examples/gno.land/r/demo/subacc/integration_test.gno @@ -10,13 +10,9 @@ import ( "gno.land/r/demo/subacc" ) -func TestEntityID(t *testing.T) { - alice := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - expected := "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/savings" - urequire.Equal(t, expected, subacc.EntityID(alice, "savings")) -} +func TestAuthIntegration(t *testing.T) { + authbankerAddr := std.DerivePkgAddr("gno.land/r/demo/authbanker") -func TestSubacc(t *testing.T) { alice := testutils.TestAddress("alice") aliceAccountID := subacc.EntityID(alice, "savings") @@ -32,14 +28,15 @@ func TestSubacc(t *testing.T) { authbanker.SendCoins(aliceToken, bobAccountID, 14) urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) urequire.Equal(t, int64(14), authbanker.GetCoins(bobAccountID)) + // urequire.Equal(t, int64(0), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount) std.TestSetRealm(std.NewUserRealm(bob)) bobToken := subacc.AuthToken("savings") - urequire.PanicsWithMessage(t, `not enough "ugnot" in account, wanted 15, got 14`, func() { authbanker.SendCoins(bobToken, alice.String(), 15) }) - /* - authbanker.SendCoins(bobToken, alice.String(), 7) - urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) - urequire.Equal(t, int64(7), authbanker.GetCoins(bobAccountID)) - urequire.Equal(t, int64(7), std.GetBanker(std.BankerTypeReadonly).GetCoins(alice)[0].Amount) - */ + urequire.PanicsWithMessage(t, "not enough in account", func() { authbanker.SendCoins(bobToken, alice.String(), 15) }) + std.TestIssueCoins(authbankerAddr, std.Coins{{"ugnot", 7}}) // we need this line because in tests, tx send does not work + authbanker.SendCoins(bobToken, alice.String(), 7) + urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) + urequire.Equal(t, int64(7), authbanker.GetCoins(bobAccountID)) + urequire.Equal(t, int64(7), std.GetBanker(std.BankerTypeReadonly).GetCoins(alice)[0].Amount) + urequire.Equal(t, 0, len(std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr))) } diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno index da6f1bb396c..4e747cf69ce 100644 --- a/examples/gno.land/r/demo/subacc/subacc.gno +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -43,7 +43,7 @@ func authenticate(autho auth.Token) string { var _ auth.AuthenticateFn = authenticate func EntityID(creator std.Address, slug string) string { - return path.Join("/", std.CurrentRealm().Addr().String(), accountID(creator, slug)) + return path.Join("/", source.Addr().String(), accountID(creator, slug)) } func accountID(creator std.Address, slug string) string { diff --git a/examples/gno.land/r/demo/subacc/subacc_test.gno b/examples/gno.land/r/demo/subacc/subacc_test.gno new file mode 100644 index 00000000000..602dbba9532 --- /dev/null +++ b/examples/gno.land/r/demo/subacc/subacc_test.gno @@ -0,0 +1,15 @@ +package subacc_test + +import ( + "std" + "testing" + + "gno.land/p/demo/urequire" + "gno.land/r/demo/subacc" +) + +func TestEntityID(t *testing.T) { + test1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + expected := "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/savings" + urequire.Equal(t, expected, subacc.EntityID(test1, "savings")) +} From 9012eec68ee81898cbfbb59710dd63f4333f9896 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:01:25 +0100 Subject: [PATCH 11/38] chore: improve test Signed-off-by: Norman --- examples/gno.land/r/demo/subacc/integration_test.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/subacc/integration_test.gno index c122fc7ee56..3dfb4ebd28f 100644 --- a/examples/gno.land/r/demo/subacc/integration_test.gno +++ b/examples/gno.land/r/demo/subacc/integration_test.gno @@ -22,13 +22,13 @@ func TestAuthIntegration(t *testing.T) { std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil) authbanker.FundVault(aliceAccountID) urequire.Equal(t, int64(42), authbanker.GetCoins(aliceAccountID)) + // urequire.Equal(t, int64(42), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount) std.TestSetRealm(std.NewUserRealm(alice)) aliceToken := subacc.AuthToken("savings") authbanker.SendCoins(aliceToken, bobAccountID, 14) urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) urequire.Equal(t, int64(14), authbanker.GetCoins(bobAccountID)) - // urequire.Equal(t, int64(0), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount) std.TestSetRealm(std.NewUserRealm(bob)) bobToken := subacc.AuthToken("savings") From 2903fcc0e513b72b8c4fd84bc9b1b6c8bf8751d2 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:03:45 +0100 Subject: [PATCH 12/38] chore: better comments Signed-off-by: Norman --- examples/gno.land/r/demo/subacc/integration_test.gno | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/subacc/integration_test.gno index 3dfb4ebd28f..b377a1cd996 100644 --- a/examples/gno.land/r/demo/subacc/integration_test.gno +++ b/examples/gno.land/r/demo/subacc/integration_test.gno @@ -22,6 +22,7 @@ func TestAuthIntegration(t *testing.T) { std.TestSetOrigSend(std.Coins{{"ugnot", 42}}, nil) authbanker.FundVault(aliceAccountID) urequire.Equal(t, int64(42), authbanker.GetCoins(aliceAccountID)) + // XXX: uncoment next line when tx send in tests is fixed // urequire.Equal(t, int64(42), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount) std.TestSetRealm(std.NewUserRealm(alice)) @@ -33,10 +34,11 @@ func TestAuthIntegration(t *testing.T) { std.TestSetRealm(std.NewUserRealm(bob)) bobToken := subacc.AuthToken("savings") urequire.PanicsWithMessage(t, "not enough in account", func() { authbanker.SendCoins(bobToken, alice.String(), 15) }) - std.TestIssueCoins(authbankerAddr, std.Coins{{"ugnot", 7}}) // we need this line because in tests, tx send does not work + std.TestIssueCoins(authbankerAddr, std.Coins{{"ugnot", 7}}) // XXX: we need this line because in tests, tx send does not work, remove when tx send in test is fixed authbanker.SendCoins(bobToken, alice.String(), 7) urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) urequire.Equal(t, int64(7), authbanker.GetCoins(bobAccountID)) urequire.Equal(t, int64(7), std.GetBanker(std.BankerTypeReadonly).GetCoins(alice)[0].Amount) - urequire.Equal(t, 0, len(std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr))) + urequire.Equal(t, 0, len(std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr))) // XXX: replace with next line when tx send in tests is fixed + // urequire.Equal(t, int64(35), std.GetBanker(std.BankerTypeReadonly).GetCoins(authbankerAddr)[0].Amount) } From 1d0ee2bc6d28959214a5dd5a532a6087e9c1ff72 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:18:51 +0100 Subject: [PATCH 13/38] chore: cleaner Signed-off-by: Norman --- .../gno.land/r/demo/authbanker/authbanker.gno | 29 ++++++++++++------- .../r/demo/subacc/integration_test.gno | 3 ++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index 47a6455a271..5a86dae8ad5 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/authreg" ) -// this example is there mostly to demonstrate auth usage, it is quite limited +// This example is there mostly to demonstrate auth usage, it is quite limited // only EOAs can fund accounts const denom = "ugnot" @@ -21,21 +21,22 @@ func GetCoins(account string) int64 { } func SendCoins(atok auth.Token, to string, amount int64) { - account := authreg.Authenticate(atok) - if amount < 1 { panic("sent amount must be >= 0") } - if vaultAmount := vaults[account]; amount > vaultAmount { - panic("not enough in account") - } - if account == to { + + from := authreg.Authenticate(atok) + + if from == to { panic("cannot send to self") } + if vaultAmount := vaults[from]; amount > vaultAmount { + panic("not enough in account") + } - vaults[account] -= amount + vaults[from] -= amount - if strings.HasPrefix(to, "/") { + if isEntityId(to) { vaults[to] += amount } else { realmBanker := std.GetBanker(std.BankerTypeRealmSend) @@ -46,11 +47,13 @@ func SendCoins(atok auth.Token, to string, amount int64) { } func FundVault(to string) { - if !strings.HasPrefix(to, "/") { + // XXX: maybe replace the following with `authreg.Validate(to)` + if !isEntityId(to) { panic("invalid destination") } - // XXX: maybe `authreg.Validate(to)` + std.AssertOriginCall() + sentCoins := std.GetOrigSend() for _, coin := range sentCoins { if coin.Denom != denom { @@ -59,3 +62,7 @@ func FundVault(to string) { vaults[to] += coin.Amount } } + +func isEntityId(str string) bool { + return strings.HasPrefix(str, "/") +} diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/subacc/integration_test.gno index b377a1cd996..de1382407f0 100644 --- a/examples/gno.land/r/demo/subacc/integration_test.gno +++ b/examples/gno.land/r/demo/subacc/integration_test.gno @@ -27,13 +27,16 @@ func TestAuthIntegration(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) aliceToken := subacc.AuthToken("savings") + authbanker.SendCoins(aliceToken, bobAccountID, 14) urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) urequire.Equal(t, int64(14), authbanker.GetCoins(bobAccountID)) std.TestSetRealm(std.NewUserRealm(bob)) bobToken := subacc.AuthToken("savings") + urequire.PanicsWithMessage(t, "not enough in account", func() { authbanker.SendCoins(bobToken, alice.String(), 15) }) + std.TestIssueCoins(authbankerAddr, std.Coins{{"ugnot", 7}}) // XXX: we need this line because in tests, tx send does not work, remove when tx send in test is fixed authbanker.SendCoins(bobToken, alice.String(), 7) urequire.Equal(t, int64(28), authbanker.GetCoins(aliceAccountID)) From 9da86f1c552b71988f21f77069c8f1403266aa1a Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:25:08 +0100 Subject: [PATCH 14/38] chore: authbanker doc Signed-off-by: Norman --- .../gno.land/r/demo/authbanker/authbanker.gno | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index 5a86dae8ad5..3c5897f322d 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -16,10 +16,14 @@ const denom = "ugnot" var vaults = make(map[string]int64) -func GetCoins(account string) int64 { - return vaults[account] +// GetCoins returns the amount of coins in the vault identified by `entityID` +func GetCoins(entityID string) int64 { + return vaults[entityID] } +// SendCoins sends `amount` coins from the vault identified by `atok` to the account identified by `to`. +// +// `to` can be an entity ID or an address. func SendCoins(atok auth.Token, to string, amount int64) { if amount < 1 { panic("sent amount must be >= 0") @@ -36,7 +40,7 @@ func SendCoins(atok auth.Token, to string, amount int64) { vaults[from] -= amount - if isEntityId(to) { + if isEntityID(to) { vaults[to] += amount } else { realmBanker := std.GetBanker(std.BankerTypeRealmSend) @@ -46,9 +50,11 @@ func SendCoins(atok auth.Token, to string, amount int64) { } } -func FundVault(to string) { +// FundVault funds the vault identified by `entityID` with the `OrigSend` coins. +// It panics if it's not an `OriginCall`. +func FundVault(entityID string) { // XXX: maybe replace the following with `authreg.Validate(to)` - if !isEntityId(to) { + if !isEntityID(entityID) { panic("invalid destination") } @@ -59,10 +65,10 @@ func FundVault(to string) { if coin.Denom != denom { panic(ufmt.Errorf("only %q supported", denom)) } - vaults[to] += coin.Amount + vaults[entityID] += coin.Amount } } -func isEntityId(str string) bool { +func isEntityID(str string) bool { return strings.HasPrefix(str, "/") } From 2a703958d8742f8b21624517da224fcebcf2f853 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 13:43:32 +0100 Subject: [PATCH 15/38] chore: add check Signed-off-by: Norman --- examples/gno.land/r/demo/authbanker/authbanker.gno | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index 3c5897f322d..d807ecdd399 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -1,6 +1,7 @@ package authbanker import ( + "errors" "std" "strings" @@ -65,6 +66,9 @@ func FundVault(entityID string) { if coin.Denom != denom { panic(ufmt.Errorf("only %q supported", denom)) } + if coin.Amount <= 0 { + panic(errors.New("send amount must be > 0")) + } vaults[entityID] += coin.Amount } } From 6195e491cdfe29a538510e3b08d6332b1c7e6718 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 14:14:53 +0100 Subject: [PATCH 16/38] chore: move integration test Signed-off-by: Norman --- .../gno.land/r/demo/{subacc => authbanker}/integration_test.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename examples/gno.land/r/demo/{subacc => authbanker}/integration_test.gno (98%) diff --git a/examples/gno.land/r/demo/subacc/integration_test.gno b/examples/gno.land/r/demo/authbanker/integration_test.gno similarity index 98% rename from examples/gno.land/r/demo/subacc/integration_test.gno rename to examples/gno.land/r/demo/authbanker/integration_test.gno index de1382407f0..2d2b2f8e4fa 100644 --- a/examples/gno.land/r/demo/subacc/integration_test.gno +++ b/examples/gno.land/r/demo/authbanker/integration_test.gno @@ -1,4 +1,4 @@ -package subacc_test +package authbanker_test import ( "std" From 1490451aa39675021798ab22dbdd5560c94a4de7 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 14:15:47 +0100 Subject: [PATCH 17/38] chore: fix package path Signed-off-by: Norman --- examples/gno.land/r/demo/authbanker/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/authbanker/gno.mod b/examples/gno.land/r/demo/authbanker/gno.mod index f687a9f3477..e4c2bf0f13f 100644 --- a/examples/gno.land/r/demo/authbanker/gno.mod +++ b/examples/gno.land/r/demo/authbanker/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/authbanker +module gno.land/r/demo/authbanker From ef0b843827a33598c7a53255b06418204ceb9681 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 15:45:38 +0100 Subject: [PATCH 18/38] chore: add nil test Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index 0542599421e..a2aee3f1659 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -8,16 +8,18 @@ import ( "gno.land/p/demo/urequire" ) -func TestValidToken(t *testing.T) { +func TestToken(t *testing.T) { urequire.NotPanics(t, func() { authenticate(getToken()) }) -} -func TestFakeToken(t *testing.T) { urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { authenticate(getFakeToken()) }) + + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + authenticate(nil) + }) } var testRealm = std.NewCodeRealm("gno.land/r/demo/absacc") From b1b68aac0fe02724b017dd9784797f432fabe363 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 26 Dec 2024 15:59:41 +0100 Subject: [PATCH 19/38] chore: explicitely handle nil tokens Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index a2aee3f1659..a9455e4707b 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -13,6 +13,10 @@ func TestToken(t *testing.T) { authenticate(getToken()) }) + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + authenticate((*token)(nil)) + }) + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { authenticate(getFakeToken()) }) @@ -20,6 +24,10 @@ func TestToken(t *testing.T) { urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { authenticate(nil) }) + + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + authenticate((*fakeToken)(nil)) + }) } var testRealm = std.NewCodeRealm("gno.land/r/demo/absacc") @@ -39,10 +47,10 @@ func getToken() auth.Token { func authenticate(autok auth.Token) string { // the next line is the core of the auth pattern, this ensures we created this token - _, ok := autok.(*token) - if !ok { + if val, ok := autok.(*token); !ok || val == nil { panic(auth.ErrInvalidToken) } + return "alice" } From 9a31a60a772822fa6ed1a941e9818a9077f27520 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 27 Dec 2024 08:18:17 +0100 Subject: [PATCH 20/38] fix: vulnerability in authreg Signed-off-by: Norman --- examples/gno.land/r/demo/authreg/authreg.gno | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index bf7407d885b..7d2a3bf2242 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -4,8 +4,10 @@ import ( "errors" "path" "std" + "strings" "gno.land/p/demo/auth" + "gno.land/p/demo/ufmt" ) var fns = make(map[string]auth.AuthenticateFn) @@ -22,5 +24,11 @@ func Authenticate(autok auth.Token) string { if !ok { panic(errors.New("unknown auth provider")) } - return path.Join("/", provider, authFn(autok)) + + subPath := path.Clean(authFn(autok)) + if strings.HasPrefix(subPath, "..") { + panic(ufmt.Errorf("invalid sub-path %q", subPath)) + } + + return path.Join("/", provider, subPath) } From ab6c3acf3f8b448876c4025bcebfc047d92954d6 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 27 Dec 2024 08:59:57 +0100 Subject: [PATCH 21/38] chore: add authreg adversarial test Signed-off-by: Norman --- examples/gno.land/r/demo/authreg/authreg.gno | 7 +- .../gno.land/r/demo/authreg/authreg_test.gno | 65 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 examples/gno.land/r/demo/authreg/authreg_test.gno diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index 7d2a3bf2242..b99809f5ad1 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -25,10 +25,13 @@ func Authenticate(autok auth.Token) string { panic(errors.New("unknown auth provider")) } - subPath := path.Clean(authFn(autok)) + return entityID(provider, authFn(autok)) +} + +func entityID(provider string, subPath string) string { + subPath = path.Clean(subPath) if strings.HasPrefix(subPath, "..") { panic(ufmt.Errorf("invalid sub-path %q", subPath)) } - return path.Join("/", provider, subPath) } diff --git a/examples/gno.land/r/demo/authreg/authreg_test.gno b/examples/gno.land/r/demo/authreg/authreg_test.gno new file mode 100644 index 00000000000..ec79dc472f2 --- /dev/null +++ b/examples/gno.land/r/demo/authreg/authreg_test.gno @@ -0,0 +1,65 @@ +package authreg + +import ( + "std" + "testing" + + "gno.land/p/demo/urequire" +) + +func TestEntityID(t *testing.T) { + cases := []struct { + name string + provider string + subPath string + res string + panicMessage string + }{ + { + name: "good", + provider: "alice", + subPath: "savings", + res: "/alice/savings", + }, + { + name: "mal_backtrack", + provider: "eve", + subPath: "../alice/savings", + panicMessage: `invalid sub-path "../alice/savings"`, + }, + { + name: "mal_backtrack_dumb", + provider: "eve", + subPath: "/../alice/savings", + res: "/eve/alice/savings", + }, + { + name: "mal_backtrack_hidden", + provider: "eve", + subPath: "todobien/very/deep/../../../../alice/savings", + panicMessage: `invalid sub-path "../alice/savings"`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + run := func() { + res := entityID(tc.provider, tc.subPath) + urequire.Equal(t, tc.res, res) + } + if tc.panicMessage != "" { + urequire.PanicsWithMessage(t, tc.panicMessage, run) + } else { + urequire.NotPanics(t, run) + } + }) + } +} + +type eveAuthToken struct { + source std.Realm +} + +func (t *eveAuthToken) Source() std.Realm { + return t.source +} From 024c5e242200268cc58fdc2fd4dc5fb13b69c1a5 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 27 Dec 2024 10:06:47 +0100 Subject: [PATCH 22/38] chore: add authbanker txtar Signed-off-by: Norman --- .../pkg/integration/testdata/authbanker.txtar | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 gno.land/pkg/integration/testdata/authbanker.txtar diff --git a/gno.land/pkg/integration/testdata/authbanker.txtar b/gno.land/pkg/integration/testdata/authbanker.txtar new file mode 100644 index 00000000000..17c60e0fc99 --- /dev/null +++ b/gno.land/pkg/integration/testdata/authbanker.txtar @@ -0,0 +1,70 @@ +adduserfrom alice 'garbage still snack cake identify girl used spoil all reform allow cargo control model spider sound urban rural avoid obvious august oil large cake' +stdout 'g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e' + +adduserfrom bob 'trap silent cross fold brisk burger teach end history sunset ignore swear proud female faculty burden upper change apart wink view stove surge answer' +stdout 'g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj' + +loadpkg gno.land/r/demo/authbanker +loadpkg gno.land/r/demo/subacc + +## start a new node +gnoland start + + +# fund alice's 'savings' sub-account +gnokey maketx call alice -send 42ugnot -pkgpath gno.land/r/demo/authbanker -func FundVault -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e/savings" -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e/savings" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '42' + +# send from alice's 'savings' sub-account to bob's 'cashier' sub-account +gnokey maketx run alice $WORK/send_to_bob_cashier.gno -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e/savings" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '28' + +# check bob account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj/cashier" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '14' + + +# send from bob's 'cashier' sub-account to bob's address +gnokey maketx run bob $WORK/send_to_bob_address.gno -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# check bob account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj/cashier" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '7' + +# XXX: check bob's address coins + +-- send_to_bob_cashier.gno -- +package main + +import ( + "std" + + "gno.land/r/demo/subacc" + "gno.land/r/demo/authbanker" +) + +func main() { + // bob's cashier account + destinationID := subacc.EntityID(std.Address("g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj"), "cashier") + + authbanker.SendCoins(subacc.AuthToken("savings"), destinationID, 14) +} + + +-- send_to_bob_address.gno -- +package main + +import ( + "gno.land/r/demo/subacc" + "gno.land/r/demo/authbanker" +) + +func main() { + authbanker.SendCoins(subacc.AuthToken("cashier"), "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj", 7) +} \ No newline at end of file From 79e39d2fbd179678fe490f6914635e66ea7a4c20 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 27 Dec 2024 10:10:44 +0100 Subject: [PATCH 23/38] chore: improve txtar spacing Signed-off-by: Norman --- gno.land/pkg/integration/testdata/authbanker.txtar | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gno.land/pkg/integration/testdata/authbanker.txtar b/gno.land/pkg/integration/testdata/authbanker.txtar index 17c60e0fc99..b524c4e37ee 100644 --- a/gno.land/pkg/integration/testdata/authbanker.txtar +++ b/gno.land/pkg/integration/testdata/authbanker.txtar @@ -18,6 +18,7 @@ gnokey maketx call alice -send 42ugnot -pkgpath gno.land/r/demo/authbanker -func gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1u5hdzuqfln65xyx5dvz9ldl9e45pmew5exg302/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e/savings" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 stdout '42' + # send from alice's 'savings' sub-account to bob's 'cashier' sub-account gnokey maketx run alice $WORK/send_to_bob_cashier.gno -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test @@ -39,6 +40,7 @@ stdout '7' # XXX: check bob's address coins + -- send_to_bob_cashier.gno -- package main From ce9ea3c107adfefeeeac24cf3baddf3fa7a087e1 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 10:37:30 +0100 Subject: [PATCH 24/38] fix: prevent vuln in subacc + cleanup Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 14 ++++ examples/gno.land/p/demo/auth/auth_test.gno | 49 +++++++++++++ examples/gno.land/r/demo/authreg/authreg.gno | 13 +--- .../gno.land/r/demo/authreg/authreg_test.gno | 72 +++++++------------ examples/gno.land/r/demo/subacc/subacc.gno | 8 +-- .../pkg/integration/testdata/authbanker.txtar | 18 +++-- 6 files changed, 107 insertions(+), 67 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index 7a3fb8b687b..d2302876cea 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -2,7 +2,11 @@ package auth import ( "errors" + "path" "std" + "strings" + + "gno.land/p/demo/ufmt" ) // Token represents an authentication token @@ -22,3 +26,13 @@ type AuthenticateFn = func(autok Token) string var ( ErrInvalidToken = errors.New("invalid token") ) + +// NamespacedEntityID generates safe entity IDs from a namespace and a sub-path. +// subPath can be user-provided and will be sanitized to prevent overriding the namespace. +func NamespacedEntityID(namespace, subPath string) string { + subPath = path.Clean(subPath) + if strings.HasPrefix(subPath, "..") { + panic(ufmt.Errorf("invalid sub-path %q", subPath)) + } + return path.Join("/", namespace, subPath) +} diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index a9455e4707b..e947a1bc733 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -68,3 +68,52 @@ var _ auth.Token = (*fakeToken)(nil) func getFakeToken() auth.Token { return &fakeToken{} } + +func TestEntityID(t *testing.T) { + cases := []struct { + name string + provider string + subPath string + res string + panicMessage string + }{ + { + name: "good", + provider: "alice", + subPath: "savings", + res: "/alice/savings", + }, + { + name: "mal_backtrack", + provider: "eve", + subPath: "../alice/savings", + panicMessage: `invalid sub-path "../alice/savings"`, + }, + { + name: "mal_backtrack_dumb", + provider: "eve", + subPath: "/../alice/savings", + res: "/eve/alice/savings", + }, + { + name: "mal_backtrack_hidden", + provider: "eve", + subPath: "todobien/very/deep/../../../../alice/savings", + panicMessage: `invalid sub-path "../alice/savings"`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + run := func() { + res := entityID(tc.provider, tc.subPath) + urequire.Equal(t, tc.res, res) + } + if tc.panicMessage != "" { + urequire.PanicsWithMessage(t, tc.panicMessage, run) + } else { + urequire.NotPanics(t, run) + } + }) + } +} diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index b99809f5ad1..f7575b63b86 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -2,12 +2,9 @@ package authreg import ( "errors" - "path" "std" - "strings" "gno.land/p/demo/auth" - "gno.land/p/demo/ufmt" ) var fns = make(map[string]auth.AuthenticateFn) @@ -25,13 +22,5 @@ func Authenticate(autok auth.Token) string { panic(errors.New("unknown auth provider")) } - return entityID(provider, authFn(autok)) -} - -func entityID(provider string, subPath string) string { - subPath = path.Clean(subPath) - if strings.HasPrefix(subPath, "..") { - panic(ufmt.Errorf("invalid sub-path %q", subPath)) - } - return path.Join("/", provider, subPath) + return auth.NamespacedEntityID(provider, authFn(autok)) } diff --git a/examples/gno.land/r/demo/authreg/authreg_test.gno b/examples/gno.land/r/demo/authreg/authreg_test.gno index ec79dc472f2..fa78cabe959 100644 --- a/examples/gno.land/r/demo/authreg/authreg_test.gno +++ b/examples/gno.land/r/demo/authreg/authreg_test.gno @@ -4,62 +4,40 @@ import ( "std" "testing" + "gno.land/p/demo/auth" "gno.land/p/demo/urequire" ) -func TestEntityID(t *testing.T) { - cases := []struct { - name string - provider string - subPath string - res string - panicMessage string - }{ - { - name: "good", - provider: "alice", - subPath: "savings", - res: "/alice/savings", - }, - { - name: "mal_backtrack", - provider: "eve", - subPath: "../alice/savings", - panicMessage: `invalid sub-path "../alice/savings"`, - }, - { - name: "mal_backtrack_dumb", - provider: "eve", - subPath: "/../alice/savings", - res: "/eve/alice/savings", - }, - { - name: "mal_backtrack_hidden", - provider: "eve", - subPath: "todobien/very/deep/../../../../alice/savings", - panicMessage: `invalid sub-path "../alice/savings"`, - }, +func TestAuthreg(t *testing.T) { + autok := &testAuthToken{source: std.NewCodeRealm("gno.land/r/testing/tester")} + unknownAutok := &testAuthToken{source: std.NewCodeRealm("gno.land/r/testing/unknown")} + called := false + mockAuthFn := func(autok auth.Token) string { + called = true + return "alice" } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - run := func() { - res := entityID(tc.provider, tc.subPath) - urequire.Equal(t, tc.res, res) - } - if tc.panicMessage != "" { - urequire.PanicsWithMessage(t, tc.panicMessage, run) - } else { - urequire.NotPanics(t, run) - } - }) - } + std.TestSetRealm(autok.Source()) + Register(mockAuthFn) + + val := Authenticate(autok) + urequire.True(t, called) + expected := ufmt.Sprintf("/%s/%s", autok.Source().Addr().String(), "alice") + urequire.Equal(t, expected, val) + + called = false + urequire.PanicsWithMessage(t, "unknown auth provider", func() { + Authenticate(unknownAutok) + }) + urequire.False(t, called) } -type eveAuthToken struct { +type testAuthToken struct { source std.Realm } -func (t *eveAuthToken) Source() std.Realm { +func (t *testAuthToken) Source() std.Realm { return t.source } + +var _ auth.Token = (*testAuthToken)(nil) diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno index 4e747cf69ce..2c973160f3c 100644 --- a/examples/gno.land/r/demo/subacc/subacc.gno +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -12,7 +12,7 @@ var source std.Realm func init() { source = std.CurrentRealm() - authreg.Register(authenticate) + authreg.Register(Authenticate) } func AuthToken(slug string) auth.Token { @@ -30,7 +30,7 @@ func (a *token) Source() std.Realm { var _ auth.Token = (*token)(nil) -func authenticate(autho auth.Token) string { +func Authenticate(autho auth.Token) string { // this check should ensure we created this object cauth, ok := autho.(*token) if !ok { @@ -40,12 +40,12 @@ func authenticate(autho auth.Token) string { return cauth.accountKey } -var _ auth.AuthenticateFn = authenticate +var _ auth.AuthenticateFn = Authenticate func EntityID(creator std.Address, slug string) string { return path.Join("/", source.Addr().String(), accountID(creator, slug)) } func accountID(creator std.Address, slug string) string { - return creator.String() + "/" + slug + return auth.NamespacedEntityID(creator.String(), slug) } diff --git a/gno.land/pkg/integration/testdata/authbanker.txtar b/gno.land/pkg/integration/testdata/authbanker.txtar index b524c4e37ee..7d4ec15136b 100644 --- a/gno.land/pkg/integration/testdata/authbanker.txtar +++ b/gno.land/pkg/integration/testdata/authbanker.txtar @@ -52,10 +52,14 @@ import ( ) func main() { - // bob's cashier account - destinationID := subacc.EntityID(std.Address("g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj"), "cashier") + autok := subacc.AuthToken("savings") - authbanker.SendCoins(subacc.AuthToken("savings"), destinationID, 14) + bobsAddr := std.Address("g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj") + bobsCashier := subacc.EntityID(bobsAddr, "cashier") + + amount := int64(14) + + authbanker.SendCoins(autok, bobsCashier, amount) } @@ -68,5 +72,11 @@ import ( ) func main() { - authbanker.SendCoins(subacc.AuthToken("cashier"), "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj", 7) + autok := subacc.AuthToken("cashier") + + bobsAddr := "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" + + amount := int64(7) + + authbanker.SendCoins(autok, bobsAddr, amount) } \ No newline at end of file From de7d7b26a2ed60f9c7e34187b08d13c56de030d7 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 10:45:54 +0100 Subject: [PATCH 25/38] chore: fix test Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 2 +- examples/gno.land/r/demo/authreg/authreg_test.gno | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index e947a1bc733..aa5e9270366 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -106,7 +106,7 @@ func TestEntityID(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run := func() { - res := entityID(tc.provider, tc.subPath) + res := auth.NamespacedEntityID(tc.provider, tc.subPath) urequire.Equal(t, tc.res, res) } if tc.panicMessage != "" { diff --git a/examples/gno.land/r/demo/authreg/authreg_test.gno b/examples/gno.land/r/demo/authreg/authreg_test.gno index fa78cabe959..83fbe11ed63 100644 --- a/examples/gno.land/r/demo/authreg/authreg_test.gno +++ b/examples/gno.land/r/demo/authreg/authreg_test.gno @@ -5,6 +5,7 @@ import ( "testing" "gno.land/p/demo/auth" + "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" ) From 607cbefbdee191c5bd1a3babb3666532d4bb61bf Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 10:51:02 +0100 Subject: [PATCH 26/38] chore: rename var Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth_test.gno | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth_test.gno b/examples/gno.land/p/demo/auth/auth_test.gno index aa5e9270366..fa919f4d070 100644 --- a/examples/gno.land/p/demo/auth/auth_test.gno +++ b/examples/gno.land/p/demo/auth/auth_test.gno @@ -72,32 +72,32 @@ func getFakeToken() auth.Token { func TestEntityID(t *testing.T) { cases := []struct { name string - provider string + namespace string subPath string res string panicMessage string }{ { - name: "good", - provider: "alice", - subPath: "savings", - res: "/alice/savings", + name: "good", + namespace: "alice", + subPath: "savings", + res: "/alice/savings", }, { name: "mal_backtrack", - provider: "eve", + namespace: "eve", subPath: "../alice/savings", panicMessage: `invalid sub-path "../alice/savings"`, }, { - name: "mal_backtrack_dumb", - provider: "eve", - subPath: "/../alice/savings", - res: "/eve/alice/savings", + name: "mal_backtrack_dumb", + namespace: "eve", + subPath: "/../alice/savings", + res: "/eve/alice/savings", }, { name: "mal_backtrack_hidden", - provider: "eve", + namespace: "eve", subPath: "todobien/very/deep/../../../../alice/savings", panicMessage: `invalid sub-path "../alice/savings"`, }, @@ -106,7 +106,7 @@ func TestEntityID(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { run := func() { - res := auth.NamespacedEntityID(tc.provider, tc.subPath) + res := auth.NamespacedEntityID(tc.namespace, tc.subPath) urequire.Equal(t, tc.res, res) } if tc.panicMessage != "" { From 4b8daf491f39294e29f6bdfbc3fee82b26b34cc6 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 11:12:05 +0100 Subject: [PATCH 27/38] feat: unregister Signed-off-by: Norman --- examples/gno.land/r/demo/authreg/authreg.gno | 4 ++++ examples/gno.land/r/demo/authreg/authreg_test.gno | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index f7575b63b86..19049def624 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -12,6 +12,10 @@ var fns = make(map[string]auth.AuthenticateFn) // XXX: we could add a slug there func Register(authenticate auth.AuthenticateFn) { caller := std.PrevRealm().Addr() + if authenticate == nil { + delete(fns, caller.String()) + return + } fns[caller.String()] = authenticate } diff --git a/examples/gno.land/r/demo/authreg/authreg_test.gno b/examples/gno.land/r/demo/authreg/authreg_test.gno index 83fbe11ed63..dce5b27a41b 100644 --- a/examples/gno.land/r/demo/authreg/authreg_test.gno +++ b/examples/gno.land/r/demo/authreg/authreg_test.gno @@ -21,16 +21,24 @@ func TestAuthreg(t *testing.T) { std.TestSetRealm(autok.Source()) Register(mockAuthFn) + // test auth val := Authenticate(autok) urequire.True(t, called) expected := ufmt.Sprintf("/%s/%s", autok.Source().Addr().String(), "alice") urequire.Equal(t, expected, val) + // test unknown provider called = false urequire.PanicsWithMessage(t, "unknown auth provider", func() { Authenticate(unknownAutok) }) urequire.False(t, called) + + // test unregistering + Register(nil) + urequire.PanicsWithMessage(t, "unknown auth provider", func() { + Authenticate(autok) + }) } type testAuthToken struct { From 129acbf21212ef80e651f8272ed7e788e8ad57aa Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 11:21:40 +0100 Subject: [PATCH 28/38] chore: improve reg Signed-off-by: Norman --- examples/gno.land/r/demo/authreg/authreg.gno | 31 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index 19049def624..48b113190bb 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -11,12 +11,30 @@ var fns = make(map[string]auth.AuthenticateFn) // XXX: we could add a slug there func Register(authenticate auth.AuthenticateFn) { - caller := std.PrevRealm().Addr() + caller := std.PrevRealm() + if caller.IsUser() { + panic("can't register from user realm") + } + callerStr := caller.Addr().String() + if authenticate == nil { - delete(fns, caller.String()) + if _, ok := fns[callerStr]; !ok { + panic(errors.New("not registered")) + } + + delete(fns, callerStr) + std.Emit( + unregisterEvent, + "caller", callerStr, + ) return } - fns[caller.String()] = authenticate + + fns[callerStr] = authenticate + std.Emit( + registerEvent, + "caller", callerStr, + ) } func Authenticate(autok auth.Token) string { @@ -26,5 +44,12 @@ func Authenticate(autok auth.Token) string { panic(errors.New("unknown auth provider")) } + // XXX: maybe emit event for audit purposes + return auth.NamespacedEntityID(provider, authFn(autok)) } + +const ( + registerEvent = "register" + unregisterEvent = "unregister" +) From faa0df809fcd6e66085669e6e32a406fe49ef3b3 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 11:38:02 +0100 Subject: [PATCH 29/38] chore: disable check due to linter bug Signed-off-by: Norman --- examples/gno.land/r/demo/authreg/authreg.gno | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index 48b113190bb..1c9b6a22f3e 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -12,9 +12,12 @@ var fns = make(map[string]auth.AuthenticateFn) // XXX: we could add a slug there func Register(authenticate auth.AuthenticateFn) { caller := std.PrevRealm() - if caller.IsUser() { - panic("can't register from user realm") - } + /* + XXX: this check makes the linter panic + if caller.IsUser() { + panic("can't register from user realm") + } + */ callerStr := caller.Addr().String() if authenticate == nil { From 9108640ecf7d3449f614317e1633c065e06a9824 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 12:23:14 +0100 Subject: [PATCH 30/38] fix: prevent nil in subacc Signed-off-by: Norman --- examples/gno.land/r/demo/subacc/subacc.gno | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno index 2c973160f3c..86bbfd44568 100644 --- a/examples/gno.land/r/demo/subacc/subacc.gno +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -30,14 +30,13 @@ func (a *token) Source() std.Realm { var _ auth.Token = (*token)(nil) -func Authenticate(autho auth.Token) string { - // this check should ensure we created this object - cauth, ok := autho.(*token) - if !ok { +func Authenticate(autok auth.Token) string { + val, ok := autok.(*token) + if !ok || val == nil { panic(auth.ErrInvalidToken) } - return cauth.accountKey + return val.accountKey } var _ auth.AuthenticateFn = Authenticate From 17433738d41ff83df6c3a9c166354efaddeefb6b Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 12:44:09 +0100 Subject: [PATCH 31/38] chore: moar doc Signed-off-by: Norman --- examples/gno.land/p/demo/auth/auth.gno | 1 + examples/gno.land/r/demo/authbanker/authbanker.gno | 8 +++++--- examples/gno.land/r/demo/authreg/authreg.gno | 4 ++++ examples/gno.land/r/demo/subacc/subacc.gno | 11 +++++++++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/p/demo/auth/auth.gno b/examples/gno.land/p/demo/auth/auth.gno index d2302876cea..f9a9cdcf820 100644 --- a/examples/gno.land/p/demo/auth/auth.gno +++ b/examples/gno.land/p/demo/auth/auth.gno @@ -1,3 +1,4 @@ +// Package auth provides object-based authentication interfaces and helpers. package auth import ( diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index d807ecdd399..0bfe08c0cff 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -1,3 +1,8 @@ +// Package authbanker provide an example auth-consumer allowing to manipulate coins with auth tokens. +// +// It is quite limited, it only supports ugnots and only EOAs can fund accounts. +// +// XXX: support grc20 to allow for realm-realm interactions package authbanker import ( @@ -10,9 +15,6 @@ import ( "gno.land/r/demo/authreg" ) -// This example is there mostly to demonstrate auth usage, it is quite limited -// only EOAs can fund accounts - const denom = "ugnot" var vaults = make(map[string]int64) diff --git a/examples/gno.land/r/demo/authreg/authreg.gno b/examples/gno.land/r/demo/authreg/authreg.gno index 1c9b6a22f3e..d4d3d17376c 100644 --- a/examples/gno.land/r/demo/authreg/authreg.gno +++ b/examples/gno.land/r/demo/authreg/authreg.gno @@ -1,3 +1,7 @@ +// Package authreg provides a permissionless authenticators registry. +// +// Authenticated entities get namespaced with the authenticator address, preventing +// malicious authenticators from impersonating another authenticator. package authreg import ( diff --git a/examples/gno.land/r/demo/subacc/subacc.gno b/examples/gno.land/r/demo/subacc/subacc.gno index 86bbfd44568..edf2459e4a0 100644 --- a/examples/gno.land/r/demo/subacc/subacc.gno +++ b/examples/gno.land/r/demo/subacc/subacc.gno @@ -1,3 +1,4 @@ +// Package subacc (short for sub-accounts) implements an authenticator based on the caller and an user-provided slug. package subacc import ( @@ -15,6 +16,10 @@ func init() { authreg.Register(Authenticate) } +// AuthToken constructs an [auth.Token] based on the caller and a slug. +// +// Returned tokens have an entity ID namespaced with the caller address, preventing +// malicious callers from impersonating another address. func AuthToken(slug string) auth.Token { caller := std.PrevRealm().Addr() return &token{accountKey: accountID(caller, slug)} @@ -30,6 +35,7 @@ func (a *token) Source() std.Realm { var _ auth.Token = (*token)(nil) +// Authenticate implements [auth.AuthenticateFn] func Authenticate(autok auth.Token) string { val, ok := autok.(*token) if !ok || val == nil { @@ -41,8 +47,9 @@ func Authenticate(autok auth.Token) string { var _ auth.AuthenticateFn = Authenticate -func EntityID(creator std.Address, slug string) string { - return path.Join("/", source.Addr().String(), accountID(creator, slug)) +// EntityID returns the full entity ID for an address and a slug +func EntityID(addr std.Address, slug string) string { + return path.Join("/", source.Addr().String(), accountID(addr, slug)) } func accountID(creator std.Address, slug string) string { From 651ab8d9d5903f3d97256c04a04c13868680080e Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 14:47:08 +0100 Subject: [PATCH 32/38] chore: add sessions example Signed-off-by: Norman --- examples/gno.land/r/demo/sessions/gno.mod | 1 + .../gno.land/r/demo/sessions/sessions.gno | 138 ++++++++++++++++++ .../r/demo/sessions/sessions_test.gno | 50 +++++++ .../testdata/authbanker_sessions.txtar | 78 ++++++++++ ...thbanker.txtar => authbanker_subacc.txtar} | 0 5 files changed, 267 insertions(+) create mode 100644 examples/gno.land/r/demo/sessions/gno.mod create mode 100644 examples/gno.land/r/demo/sessions/sessions.gno create mode 100644 examples/gno.land/r/demo/sessions/sessions_test.gno create mode 100644 gno.land/pkg/integration/testdata/authbanker_sessions.txtar rename gno.land/pkg/integration/testdata/{authbanker.txtar => authbanker_subacc.txtar} (100%) diff --git a/examples/gno.land/r/demo/sessions/gno.mod b/examples/gno.land/r/demo/sessions/gno.mod new file mode 100644 index 00000000000..1e92e0a4420 --- /dev/null +++ b/examples/gno.land/r/demo/sessions/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/sessions diff --git a/examples/gno.land/r/demo/sessions/sessions.gno b/examples/gno.land/r/demo/sessions/sessions.gno new file mode 100644 index 00000000000..085d717d907 --- /dev/null +++ b/examples/gno.land/r/demo/sessions/sessions.gno @@ -0,0 +1,138 @@ +// Package sessions implements an authenticator that allows a mother address to +// whitelist session addresses that can then act as the mother address using auth tokens. +package sessions + +import ( + "errors" + "std" + "time" + + "gno.land/p/demo/auth" + "gno.land/r/demo/authreg" +) + +var ( + sessions map[string]map[string]*time.Time + source std.Realm +) + +func init() { + sessions = make(map[string]map[string]*time.Time) + source = std.CurrentRealm() + + authreg.Register(Authenticate) +} + +// Login creates a session allowing sessionAddr to act as the caller until we're past the expiry time. +// A nil expiry create a session with no expiration +func Login(sessionAddr std.Address, unixSecondsExpiry int64) { + if !sessionAddr.IsValid() { + panic(errors.New("invalid session address")) + } + + caller := std.PrevRealm().Addr() + if caller == sessionAddr { + panic(errors.New("can't login as self")) + } + + if unixSecondsExpiry < 0 { + panic(errors.New("negative expiry")) + } + expiry := (*time.Time)(nil) + if unixSecondsExpiry > 0 { + timeExpiry := time.Unix(unixSecondsExpiry, 0) + expiry = &timeExpiry + if !expiry.After(time.Now()) { + panic(errors.New("expiry is now or in the past")) + } + } + + callerStr := caller.String() + if _, ok := sessions[callerStr]; !ok { + sessions[callerStr] = make(map[string]*time.Time) + } + + sessions[callerStr][sessionAddr.String()] = expiry +} + +// Logout allows to delete sessions. +// +// The caller must be one of addr or sessionAddr. +// addr is the mother address. +// sessionAddr is the allowed address. +// +// If the caller is addr and a zero sessionAddr is passed, Logout will delete all sessions for addr +func Logout(addr std.Address, sessionAddr std.Address) { + caller := std.PrevRealm().Addr() + if caller != sessionAddr && caller != addr { + panic(errors.New("caller must be addr or sessionAddr")) + } + + addrSessions, ok := sessions[addr.String()] + if !ok { + panic(errors.New("addr has no sessions")) + } + + // delete all sessions if requested + if sessionAddr == std.Address("") { + delete(sessions, addr.String()) + return + } + + if _, ok := addrSessions[sessionAddr.String()]; !ok { + panic(errors.New("sessionAddr is not a session of addr")) + } + // delete all sessions if it was the last one + if len(addrSessions) == 1 { + delete(sessions, addr.String()) + return + } + + delete(addrSessions, sessionAddr.String()) +} + +// AuthToken constructs a session token. +// `as` is the mother address and the caller is the allowed address. +// +// It is not validated at creation. +// We need to validate at authentication anyway since prevalidated tokens could be stored and reused after the session is deleted or expired. +// +// A token is always considered valid if caller == as +func AuthToken(as std.Address) auth.Token { + return &token{ + addr: as, + sessionAddr: std.PrevRealm().Addr(), + } +} + +// Authenticate validates a token generated by [AuthToken]. It panics if the session does not exists or is expired +func Authenticate(autok auth.Token) string { + val, ok := autok.(*token) + if !ok || val == nil { + panic(auth.ErrInvalidToken) + } + + addr := val.addr.String() + sessionAddr := val.sessionAddr.String() + + if addr != sessionAddr { + expiry, ok := sessions[addr][sessionAddr] + if !ok { + panic(auth.ErrInvalidToken) + } + if expiry != nil && !time.Now().Before(*expiry) { + panic(auth.ErrInvalidToken) + } + } + + return "/" + addr +} + +type token struct { + addr std.Address + sessionAddr std.Address +} + +func (t *token) Source() std.Realm { + return source +} diff --git a/examples/gno.land/r/demo/sessions/sessions_test.gno b/examples/gno.land/r/demo/sessions/sessions_test.gno new file mode 100644 index 00000000000..9f218cbd855 --- /dev/null +++ b/examples/gno.land/r/demo/sessions/sessions_test.gno @@ -0,0 +1,50 @@ +package sessions + +import ( + "std" + "testing" + + "gno.land/p/demo/auth" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +// TODO: more extensive tests + +func TestSessions(t *testing.T) { + aliceAddr := testutils.TestAddress("alice") + aliceRealm := std.NewUserRealm(aliceAddr) + + bobAddr := testutils.TestAddress("bob") + bobRealm := std.NewUserRealm(bobAddr) + + // no session created + { + std.TestSetRealm(bobRealm) + + autok := AuthToken(aliceAddr) + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + Authenticate(autok) + }) + } + + // happy path + { + std.TestSetRealm(aliceRealm) + + Login(bobAddr, nil) + + std.TestSetRealm(bobRealm) + + autok := AuthToken(aliceAddr) + entityID := Authenticate(autok) + urequire.Equal(t, "/"+aliceAddr.String(), entityID) + + Logout(aliceAddr, bobAddr) + + autok = AuthToken(aliceAddr) + urequire.PanicsWithMessage(t, auth.ErrInvalidToken.Error(), func() { + Authenticate(autok) + }) + } +} diff --git a/gno.land/pkg/integration/testdata/authbanker_sessions.txtar b/gno.land/pkg/integration/testdata/authbanker_sessions.txtar new file mode 100644 index 00000000000..8f6bb75375e --- /dev/null +++ b/gno.land/pkg/integration/testdata/authbanker_sessions.txtar @@ -0,0 +1,78 @@ +adduserfrom alice 'garbage still snack cake identify girl used spoil all reform allow cargo control model spider sound urban rural avoid obvious august oil large cake' +stdout 'g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e' + +adduserfrom bob 'trap silent cross fold brisk burger teach end history sunset ignore swear proud female faculty burden upper change apart wink view stove surge answer' +stdout 'g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj' + +loadpkg gno.land/r/demo/authbanker +loadpkg gno.land/r/demo/sessions + +## start a new node +gnoland start + + +# fund alice's sessions account +gnokey maketx call alice -send 42ugnot -pkgpath gno.land/r/demo/authbanker -func FundVault -args "/g1w4yzwsdjge80nvltnrrh6c66sm607lr828j32f/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1w4yzwsdjge80nvltnrrh6c66sm607lr828j32f/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '42' + +# login, allowing bob to act as alice indefinitely +gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func Login -args "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" -args "0" -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# as bob, send from alice's session account +gnokey maketx run bob $WORK/send_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 25000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g1w4yzwsdjge80nvltnrrh6c66sm607lr828j32f/g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '28' + +# XXX: check bob's address coins + +# logout +gnokey maketx call bob -pkgpath gno.land/r/demo/sessions -func Logout -args "g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -args "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# as bob, try send from alice's session account, should fail +! gnokey maketx run bob $WORK/send_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + + +-- send_to_bob.gno -- +package main + +import ( + "std" + + "gno.land/r/demo/sessions" + "gno.land/r/demo/authbanker" +) + +func main() { + aliceAddr := std.Address("g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e") + autok := sessions.AuthToken(aliceAddr) + + bobsAddr := "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" + + amount := int64(14) + + authbanker.SendCoins(autok, bobsAddr, amount) +} + + +-- send_to_bob_address.gno -- +package main + +import ( + "gno.land/r/demo/subacc" + "gno.land/r/demo/authbanker" +) + +func main() { + autok := subacc.AuthToken("cashier") + + bobsAddr := "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" + + amount := int64(7) + + authbanker.SendCoins(autok, bobsAddr, amount) +} \ No newline at end of file diff --git a/gno.land/pkg/integration/testdata/authbanker.txtar b/gno.land/pkg/integration/testdata/authbanker_subacc.txtar similarity index 100% rename from gno.land/pkg/integration/testdata/authbanker.txtar rename to gno.land/pkg/integration/testdata/authbanker_subacc.txtar From e9f39fb70a264a95f27ca39e52f3e193fd28d507 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 14:47:45 +0100 Subject: [PATCH 33/38] chore: remove dev artifact Signed-off-by: Norman --- .../testdata/authbanker_sessions.txtar | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/gno.land/pkg/integration/testdata/authbanker_sessions.txtar b/gno.land/pkg/integration/testdata/authbanker_sessions.txtar index 8f6bb75375e..8e58b165f07 100644 --- a/gno.land/pkg/integration/testdata/authbanker_sessions.txtar +++ b/gno.land/pkg/integration/testdata/authbanker_sessions.txtar @@ -55,24 +55,5 @@ func main() { amount := int64(14) - authbanker.SendCoins(autok, bobsAddr, amount) -} - - --- send_to_bob_address.gno -- -package main - -import ( - "gno.land/r/demo/subacc" - "gno.land/r/demo/authbanker" -) - -func main() { - autok := subacc.AuthToken("cashier") - - bobsAddr := "g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj" - - amount := int64(7) - authbanker.SendCoins(autok, bobsAddr, amount) } \ No newline at end of file From 68b6e6bdd18dadee1f7a06d00420f5df719d3ada Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 14:58:41 +0100 Subject: [PATCH 34/38] fix: test Signed-off-by: Norman --- examples/gno.land/r/demo/sessions/sessions.gno | 2 ++ examples/gno.land/r/demo/sessions/sessions_test.gno | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/sessions/sessions.gno b/examples/gno.land/r/demo/sessions/sessions.gno index 085d717d907..e4e9b129cfa 100644 --- a/examples/gno.land/r/demo/sessions/sessions.gno +++ b/examples/gno.land/r/demo/sessions/sessions.gno @@ -16,6 +16,8 @@ var ( source std.Realm ) +const NoExpiry = int64(0) + func init() { sessions = make(map[string]map[string]*time.Time) source = std.CurrentRealm() diff --git a/examples/gno.land/r/demo/sessions/sessions_test.gno b/examples/gno.land/r/demo/sessions/sessions_test.gno index 9f218cbd855..361e6865b09 100644 --- a/examples/gno.land/r/demo/sessions/sessions_test.gno +++ b/examples/gno.land/r/demo/sessions/sessions_test.gno @@ -32,7 +32,7 @@ func TestSessions(t *testing.T) { { std.TestSetRealm(aliceRealm) - Login(bobAddr, nil) + Login(bobAddr, NoExpiry) std.TestSetRealm(bobRealm) From e3d5f788b75a9ee5f23a9a4f6550ff4f182ae507 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 15:19:57 +0100 Subject: [PATCH 35/38] chore: add sessions.EntityID helper Signed-off-by: Norman --- examples/gno.land/r/demo/sessions/sessions.gno | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/gno.land/r/demo/sessions/sessions.gno b/examples/gno.land/r/demo/sessions/sessions.gno index e4e9b129cfa..4685fcad11d 100644 --- a/examples/gno.land/r/demo/sessions/sessions.gno +++ b/examples/gno.land/r/demo/sessions/sessions.gno @@ -130,6 +130,11 @@ func Authenticate(autok auth.Token) string { return "/" + addr } +// EntityID returns the full entity ID for an address +func EntityID(addr std.Address) string { + return "/" + source.Addr().String() + "/" + addr.String() +} + type token struct { addr std.Address sessionAddr std.Address From 7c835b4ab6b86b7724aab478419cf950a6012e99 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 19:08:03 +0100 Subject: [PATCH 36/38] chore: abstract account Signed-off-by: Norman --- examples/gno.land/r/demo/absacc/absacc.gno | 145 ++++++++++++++++++ examples/gno.land/r/demo/absacc/gno.mod | 1 + .../gno.land/r/demo/authbanker/authbanker.gno | 2 +- .../testdata/authbanker_absacc.txtar | 91 +++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 examples/gno.land/r/demo/absacc/absacc.gno create mode 100644 examples/gno.land/r/demo/absacc/gno.mod create mode 100644 gno.land/pkg/integration/testdata/authbanker_absacc.txtar diff --git a/examples/gno.land/r/demo/absacc/absacc.gno b/examples/gno.land/r/demo/absacc/absacc.gno new file mode 100644 index 00000000000..d3b443c7c69 --- /dev/null +++ b/examples/gno.land/r/demo/absacc/absacc.gno @@ -0,0 +1,145 @@ +package absacc + +import ( + "errors" + "std" + + "gno.land/p/demo/auth" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/authreg" + "gno.land/r/demo/sessions" +) + +var ( + source std.Realm + id seqid.ID + accounts = make(map[seqid.ID]*account) +) + +func init() { + source = std.CurrentRealm() + authreg.Register(Authenticate) +} + +func CreateCallerAccount() seqid.ID { + authorities := []Authority{ + { + Provider: std.DerivePkgAddr("gno.land/r/demo/sessions"), + EntityID: sessions.EntityID(std.PrevRealm().Addr()), + }, + } + return CreateAccount(authorities...) +} + +func CreateAccount(authorities ...Authority) seqid.ID { + if len(authorities) == 0 { + panic(errors.New("must provide at least one authority, otherwise the account is locked")) + } + + accountID := id.Next() + acc := account{ + authorities: make(map[std.Address]map[string]struct{}), + } + acc.addAuthorities(authorities...) + + accounts[accountID] = &acc + return accountID +} + +func AddAuthorities(autok auth.Token, authorities ...Authority) { + account := authenticateAccount(autok) + account.addAuthorities(authorities...) +} + +func RemoveAuthorities(autok auth.Token, authorities ...Authority) { + account := authenticateAccount(autok) + account.removeAuthorities(authorities...) +} + +func AuthToken(accountID seqid.ID, subToken auth.Token) auth.Token { + if _, ok := accounts[accountID]; !ok { + panic(errors.New("unknown account")) + } + return &token{ + accountID: accountID, + subToken: subToken, + } +} + +func Authenticate(autok auth.Token) string { + return ufmt.Sprintf("%d", uint64(authenticateID(autok))) +} + +func EntityID(accountID seqid.ID) string { + return ufmt.Sprintf("/%s/%d", source.Addr().String(), uint64(accountID)) +} + +func authenticateID(autok auth.Token) seqid.ID { + val, ok := autok.(*token) + if !ok || val == nil { + panic(auth.ErrInvalidToken) + } + + entityID := authreg.Authenticate(val.subToken) + provider := val.subToken.Source().Addr() + if _, ok := accounts[val.accountID].authorities[provider][entityID]; !ok { + panic(auth.ErrInvalidToken) + } + + return val.accountID +} + +func authenticateAccount(autok auth.Token) *account { + return accounts[authenticateID(autok)] +} + +type Authority struct { + Provider std.Address + EntityID string +} + +type token struct { + accountID seqid.ID + subToken auth.Token +} + +func (t *token) Source() std.Realm { + return source +} + +type account struct { + authorities map[std.Address]map[string]struct{} +} + +func (a *account) addAuthorities(authorities ...Authority) { + for _, authority := range authorities { + if _, ok := a.authorities[authority.Provider]; !ok { + a.authorities[authority.Provider] = make(map[string]struct{}) + } + a.authorities[authority.Provider][authority.EntityID] = struct{}{} + } +} + +func (a *account) removeAuthorities(authorities ...Authority) { + for _, authority := range authorities { + if _, ok := a.authorities[authority.Provider]; !ok { + continue + } + if authority.EntityID == "" { + delete(a.authorities, authority.Provider) + continue + } + if _, ok := a.authorities[authority.Provider][authority.EntityID]; !ok { + continue + } + if len(a.authorities[authority.Provider]) == 1 { + delete(a.authorities, authority.Provider) + } else { + delete(a.authorities[authority.Provider], authority.EntityID) + } + } + if len(a.authorities) == 1 { + panic(errors.New("must keep at least one authority, otherwise the account will be locked")) + } +} diff --git a/examples/gno.land/r/demo/absacc/gno.mod b/examples/gno.land/r/demo/absacc/gno.mod new file mode 100644 index 00000000000..b11da9335c1 --- /dev/null +++ b/examples/gno.land/r/demo/absacc/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/absacc diff --git a/examples/gno.land/r/demo/authbanker/authbanker.gno b/examples/gno.land/r/demo/authbanker/authbanker.gno index 0bfe08c0cff..5715764233c 100644 --- a/examples/gno.land/r/demo/authbanker/authbanker.gno +++ b/examples/gno.land/r/demo/authbanker/authbanker.gno @@ -38,7 +38,7 @@ func SendCoins(atok auth.Token, to string, amount int64) { panic("cannot send to self") } if vaultAmount := vaults[from]; amount > vaultAmount { - panic("not enough in account") + panic(ufmt.Errorf("not enough in account %q", from)) } vaults[from] -= amount diff --git a/gno.land/pkg/integration/testdata/authbanker_absacc.txtar b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar new file mode 100644 index 00000000000..630db5d4a4f --- /dev/null +++ b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar @@ -0,0 +1,91 @@ +adduserfrom alice 'garbage still snack cake identify girl used spoil all reform allow cargo control model spider sound urban rural avoid obvious august oil large cake' +stdout 'g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e' + +adduserfrom bob 'trap silent cross fold brisk burger teach end history sunset ignore swear proud female faculty burden upper change apart wink view stove surge answer' +stdout 'g1gvcsfj492u6cpxudrprzz23ulmppxgv9pqmnpj' + +adduserfrom alice-session 'flee junk glad guitar require truth exhibit visit absurd february town decrease lobster pear mix path average depart wet rebel detail conduct often salon' +stdout 'g15e6e70nujtydkwt655k68z8ytzytu4ys6q4typ' + +loadpkg gno.land/r/demo/authbanker +loadpkg gno.land/r/demo/absacc +loadpkg gno.land/r/demo/sessions + +## start a new node +gnoland start + + +# create alice's abstract account +gnokey maketx call alice -pkgpath gno.land/r/demo/absacc -func CreateCallerAccount -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test +stdout '1' + +# create bob's abstract account +gnokey maketx call alice -pkgpath gno.land/r/demo/absacc -func CreateCallerAccount -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test +stdout '2' + + + +# fund alice's abstract account +gnokey maketx call alice -send 42ugnot -pkgpath gno.land/r/demo/authbanker -func FundVault -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/1" -gas-fee 1000000ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/1" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '42' + + +# send from alice's abstract account to bob's abstract account +gnokey maketx run alice $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/1" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '28' + +# check bob account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/2" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '14' + + +# create session +gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Login" -args "g15e6e70nujtydkwt655k68z8ytzytu4ys6q4typ" -args "0" -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test + +# send from alice's session to bob's abstract account +gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test + +# check alice account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/1" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '14' + +# check bob account +gnokey maketx call -pkgpath gno.land/r/demo/authbanker -func "GetCoins" -args "/g147gkrl6u5txjcvqpcm8ucamscg3f6qdnkht3lp/2" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '28' + + +# delete sessions +gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Logout" -args "g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -args "" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test + +# try to send from alice's session to bob's abstract account, should fail +! gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test +stderr 'invalid token' + + +-- send_from_alice_to_bob.gno -- +package main + +import ( + "std" + + "gno.land/r/demo/absacc" + "gno.land/r/demo/sessions" + "gno.land/r/demo/authbanker" +) + +func main() { + aliceAddr := std.Address("g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e") + autok := absacc.AuthToken(1, sessions.AuthToken(aliceAddr)) + + bobsAbstractAccount := absacc.EntityID(2) + + amount := int64(14) + + authbanker.SendCoins(autok, bobsAbstractAccount, amount) +} \ No newline at end of file From eae728214e705439234f10419ae27a0354dc30f0 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 19:13:34 +0100 Subject: [PATCH 37/38] chore: clearer txtar comments Signed-off-by: Norman --- gno.land/pkg/integration/testdata/authbanker_absacc.txtar | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/integration/testdata/authbanker_absacc.txtar b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar index 630db5d4a4f..49702203f2c 100644 --- a/gno.land/pkg/integration/testdata/authbanker_absacc.txtar +++ b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar @@ -48,7 +48,7 @@ stdout '14' # create session gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Login" -args "g15e6e70nujtydkwt655k68z8ytzytu4ys6q4typ" -args "0" -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test -# send from alice's session to bob's abstract account +# send from alice's account via a session to bob's abstract account gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test # check alice account @@ -63,7 +63,7 @@ stdout '28' # delete sessions gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Logout" -args "g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -args "" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -# try to send from alice's session to bob's abstract account, should fail +# try to send from alice's account via a session, should fail ! gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test stderr 'invalid token' From 23e45a3867be982dfe4eff76c54943a0409917d0 Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 28 Dec 2024 19:25:28 +0100 Subject: [PATCH 38/38] chore: explicit comments Signed-off-by: Norman --- gno.land/pkg/integration/testdata/authbanker_absacc.txtar | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/integration/testdata/authbanker_absacc.txtar b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar index 49702203f2c..e9c7c7b4366 100644 --- a/gno.land/pkg/integration/testdata/authbanker_absacc.txtar +++ b/gno.land/pkg/integration/testdata/authbanker_absacc.txtar @@ -48,7 +48,7 @@ stdout '14' # create session gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Login" -args "g15e6e70nujtydkwt655k68z8ytzytu4ys6q4typ" -args "0" -gas-fee 1000000ugnot -gas-wanted 20000000 -broadcast -chainid=tendermint_test -# send from alice's account via a session to bob's abstract account +# send from alice's abstract account via a session to bob's abstract account gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test # check alice account @@ -63,7 +63,7 @@ stdout '28' # delete sessions gnokey maketx call alice -pkgpath gno.land/r/demo/sessions -func "Logout" -args "g1xd348e34sg4ndt3shpepv85dq8zw8dlufp9t6e" -args "" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -# try to send from alice's account via a session, should fail +# try to send from alice's abstract account via a session, should fail ! gnokey maketx run alice-session $WORK/send_from_alice_to_bob.gno -gas-fee 1000000ugnot -gas-wanted 30000000 -broadcast -chainid=tendermint_test stderr 'invalid token'