Search Apps Documentation Source Content File Folder Download Copy Actions Download

authz_test.gno

20.42 Kb · 602 lines
  1package authz
  2
  3import (
  4	"chain"
  5	"chain/runtime/unsafe"
  6	"errors"
  7	"strings"
  8	"testing"
  9
 10	"gno.land/p/nt/testutils/v0"
 11	"gno.land/p/nt/uassert/v0"
 12)
 13
 14func TestNewWithCurrent(cur realm, t *testing.T) {
 15	alice := testutils.TestAddress("alice")
 16	testing.SetRealm(testing.NewUserRealm(alice))
 17
 18	auth := NewWithMembers(cur.Address())
 19
 20	// Check that the current authority is a MemberAuthority
 21	memberAuth, ok := auth.Authority().(*MemberAuthority)
 22	uassert.True(t, ok, "expected MemberAuthority")
 23
 24	// Check that the caller is a member
 25	uassert.True(t, memberAuth.Has(alice), "caller should be a member")
 26
 27	// Check string representation
 28	uassert.True(t, strings.Contains(auth.String(), alice.String()))
 29}
 30
 31func TestNewWithAuthority(cur realm, t *testing.T) {
 32	alice := testutils.TestAddress("alice")
 33	memberAuth := NewMemberAuthority(alice)
 34
 35	auth := NewWithAuthority(memberAuth)
 36
 37	// Check that the current authority is the one we provided
 38	uassert.True(t, auth.Authority() == memberAuth, "expected provided authority")
 39}
 40
 41func TestAuthorizerAuthorize(cur realm, t *testing.T) {
 42	alice := testutils.TestAddress("alice")
 43	testing.SetRealm(testing.NewUserRealm(alice))
 44
 45	auth := NewWithMembers(cur.Address())
 46
 47	// Test successful action with args
 48	executed := false
 49	args := []any{"test_arg", 123}
 50	err := auth.DoByCurrent(0, cur, "test_action", func() error {
 51		executed = true
 52		return nil
 53	}, args...)
 54
 55	uassert.True(t, err == nil, "expected no error")
 56	uassert.True(t, executed, "action should have been executed")
 57
 58	// Test unauthorized action with args
 59	testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("bob")))
 60
 61	executed = false
 62	err = auth.DoByCurrent(0, cur, "test_action", func() error {
 63		executed = true
 64		return nil
 65	}, "unauthorized_arg")
 66
 67	uassert.True(t, err != nil, "expected error")
 68	uassert.False(t, executed, "action should not have been executed")
 69
 70	// Test action returning error
 71	testing.SetRealm(testing.NewUserRealm(alice))
 72	expectedErr := errors.New("test error")
 73
 74	err = auth.DoByCurrent(0, cur, "test_action", func() error {
 75		return expectedErr
 76	})
 77
 78	uassert.True(t, err == expectedErr, "expected specific error")
 79}
 80
 81func TestAuthorizerTransfer(cur realm, t *testing.T) {
 82	alice := testutils.TestAddress("alice")
 83	testing.SetRealm(testing.NewUserRealm(alice))
 84
 85	auth := NewWithMembers(cur.Address())
 86
 87	// Test transfer to new member authority
 88	bob := testutils.TestAddress("bob")
 89	newAuth := NewMemberAuthority(bob)
 90
 91	var err error
 92	func(cur realm) { err = auth.Transfer(0, cur, newAuth) }(cross(cur))
 93	uassert.True(t, err == nil, "expected no error")
 94	uassert.True(t, auth.Authority() == newAuth, "expected new authority")
 95
 96	// Test unauthorized transfer: principal is not a member of newAuth.
 97	carol := testutils.TestAddress("carol")
 98	testing.SetRealm(testing.NewUserRealm(carol))
 99
100	func(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))
101	uassert.True(t, err != nil, "expected error")
102
103	// Test transfer to contract authority — bob is the current authority.
104	testing.SetRealm(testing.NewUserRealm(bob))
105	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
106		return action()
107	})
108
109	func(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))
110	uassert.True(t, err == nil, "expected no error")
111	uassert.True(t, auth.Authority() == contractAuth, "expected contract authority")
112}
113
114func TestAuthorizerTransferChain(cur realm, t *testing.T) {
115	alice := testutils.TestAddress("alice")
116	testing.SetRealm(testing.NewUserRealm(alice))
117
118	// Create a chain of transfers
119	auth := NewWithMembers(cur.Address())
120
121	// First transfer to a new member authority
122	bob := testutils.TestAddress("bob")
123	memberAuth := NewMemberAuthority(bob)
124
125	var err error
126	func(cur realm) { err = auth.Transfer(0, cur, memberAuth) }(cross(cur))
127	uassert.True(t, err == nil, "unexpected error in first transfer")
128
129	// Then transfer to a contract authority — bob is now the authority.
130	testing.SetRealm(testing.NewUserRealm(bob))
131	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
132		return action()
133	})
134	func(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))
135	uassert.True(t, err == nil, "unexpected error in second transfer")
136
137	// Finally transfer to an auto-accept authority — must come from the
138	// contract realm so ContractAuthority's wrappedAction CurrentRealm
139	// check passes. Use the test frame's cur directly (SetRealm mutates
140	// it in place); a crossing closure would push a new frame whose cur
141	// is the test package, breaking the runtime.CurrentRealm match.
142	autoAuth := NewAutoAcceptAuthority()
143	codeRealm := testing.NewCodeRealm("gno.land/r/test")
144	testing.SetRealm(codeRealm)
145	err = auth.Transfer(0, cur, autoAuth)
146	uassert.True(t, err == nil, "unexpected error in final transfer")
147	uassert.True(t, auth.Authority() == autoAuth, "expected auto-accept authority")
148}
149
150func TestAuthorizerTransferUnauthorizedRejected(cur realm, t *testing.T) {
151	// Regression for the address-parameter forgery: previously Transfer
152	// took a caller-supplied `caller address` that an attacker realm could
153	// set to the real owner. After the IsCurrent + rlm.Previous() fix, the
154	// principal is the captured cur's previous and cannot be forged.
155	admin := testutils.TestAddress("admin")
156	attacker := testutils.TestAddress("attacker")
157
158	testing.SetRealm(testing.NewUserRealm(admin))
159	auth := NewWithMembers(cur.Address()) // admin is the initial authority
160
161	initialAuth, ok := auth.Authority().(*MemberAuthority)
162	uassert.True(t, ok)
163	uassert.True(t, initialAuth.Has(admin))
164	uassert.False(t, initialAuth.Has(attacker))
165
166	// Attacker context: cur.Previous() inside the closure will be attacker.
167	testing.SetRealm(testing.NewUserRealm(attacker))
168	attackerAuth := NewMemberAuthority(attacker)
169	var err error
170	func(cur realm) { err = auth.Transfer(0, cur, attackerAuth) }(cross(cur))
171
172	uassert.True(t, err != nil, "attacker transfer must be rejected")
173
174	// Authority unchanged: still the initial admin-only MemberAuthority.
175	finalAuth, ok := auth.Authority().(*MemberAuthority)
176	uassert.True(t, ok)
177	uassert.True(t, finalAuth == initialAuth, "authority must not have changed")
178	uassert.True(t, finalAuth.Has(admin))
179	uassert.False(t, finalAuth.Has(attacker))
180}
181
182func TestAuthorizerWithDroppedAuthority(cur realm, t *testing.T) {
183	alice := testutils.TestAddress("alice")
184	testing.SetRealm(testing.NewUserRealm(alice))
185
186	auth := NewWithMembers(cur.Address())
187
188	// Transfer to dropped authority
189	var err error
190	func(cur realm) { err = auth.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))
191	uassert.True(t, err == nil, "expected no error")
192
193	// Try to execute action
194	err = auth.DoByCurrent(0, cur, "test_action", func() error {
195		return nil
196	})
197	uassert.True(t, err != nil, "expected error from dropped authority")
198
199	// Try to transfer again
200	func(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))
201	uassert.True(t, err != nil, "expected error when transferring from dropped authority")
202}
203
204func TestContractAuthorityHandlerExecutionOnce(cur realm, t *testing.T) {
205	attempts := 0
206	executed := 0
207
208	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
209		// Try to execute the action twice in the same handler
210		if err := action(); err != nil {
211			return err
212		}
213		attempts++
214
215		// Second execution should fail
216		if err := action(); err != nil {
217			return err
218		}
219		attempts++
220		return nil
221	})
222
223	// Set caller to contract address
224	codeRealm := testing.NewCodeRealm("gno.land/r/test")
225	testing.SetRealm(codeRealm)
226	code := codeRealm.Address()
227
228	testArgs := []any{"proposal_id", 42, "metadata", map[string]string{"key": "value"}}
229	err := contractAuth.Authorize(code, "test_action", func() error {
230		executed++
231		return nil
232	}, testArgs...)
233
234	uassert.True(t, err == nil, "handler execution should succeed")
235	uassert.True(t, attempts == 2, "handler should have attempted execution twice")
236	uassert.True(t, executed == 1, "handler should have executed once")
237}
238
239func TestContractAuthorityExecutionTwice(cur realm, t *testing.T) {
240	executed := 0
241
242	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
243		return action()
244	})
245
246	// Set caller to contract address
247	codeRealm := testing.NewCodeRealm("gno.land/r/test")
248	testing.SetRealm(codeRealm)
249	code := codeRealm.Address()
250	testArgs := []any{"proposal_id", 42, "metadata", map[string]string{"key": "value"}}
251
252	err := contractAuth.Authorize(code, "test_action", func() error {
253		executed++
254		return nil
255	}, testArgs...)
256
257	uassert.True(t, err == nil, "handler execution should succeed")
258	uassert.True(t, executed == 1, "handler should have executed once")
259
260	// A new action, even with the same title, should be executed
261	err = contractAuth.Authorize(code, "test_action", func() error {
262		executed++
263		return nil
264	}, testArgs...)
265
266	uassert.True(t, err == nil, "handler execution should succeed")
267	uassert.True(t, executed == 2, "handler should have executed twice")
268}
269
270func TestContractAuthorityWithProposer(cur realm, t *testing.T) {
271	alice := testutils.TestAddress("alice")
272	memberAuth := NewMemberAuthority(alice)
273
274	handlerCalled := false
275	actionExecuted := false
276
277	contractAuth := NewRestrictedContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
278		handlerCalled = true
279		// Set caller to contract address before executing action
280		testing.SetRealm(testing.NewCodeRealm("gno.land/r/test"))
281		return action()
282	}, memberAuth)
283
284	// Test authorized member
285	testArgs := []any{"proposal_metadata", "test value"}
286	err := contractAuth.Authorize(alice, "test_action", func() error {
287		actionExecuted = true
288		return nil
289	}, testArgs...)
290
291	uassert.True(t, err == nil, "authorized member should be able to propose")
292	uassert.True(t, handlerCalled, "contract handler should be called")
293	uassert.True(t, actionExecuted, "action should be executed")
294
295	// Reset flags for unauthorized test
296	handlerCalled = false
297	actionExecuted = false
298
299	// Test unauthorized proposer
300	bob := testutils.TestAddress("bob")
301	err = contractAuth.Authorize(bob, "test_action", func() error {
302		actionExecuted = true
303		return nil
304	}, testArgs...)
305
306	uassert.True(t, err != nil, "unauthorized member should not be able to propose")
307	uassert.False(t, handlerCalled, "contract handler should not be called for unauthorized proposer")
308	uassert.False(t, actionExecuted, "action should not be executed for unauthorized proposer")
309}
310
311func TestAutoAcceptAuthority(cur realm, t *testing.T) {
312	alice := testutils.TestAddress("alice")
313	auth := NewAutoAcceptAuthority()
314
315	// Test that any action is authorized
316	executed := false
317	err := auth.Authorize(alice, "test_action", func() error {
318		executed = true
319		return nil
320	})
321
322	uassert.True(t, err == nil, "auto-accept should not return error")
323	uassert.True(t, executed, "action should have been executed")
324
325	// Test with different caller
326	random := testutils.TestAddress("random")
327	executed = false
328	err = auth.Authorize(random, "test_action", func() error {
329		executed = true
330		return nil
331	})
332
333	uassert.True(t, err == nil, "auto-accept should not care about caller")
334	uassert.True(t, executed, "action should have been executed")
335}
336
337func TestAutoAcceptAuthorityWithArgs(cur realm, t *testing.T) {
338	auth := NewAutoAcceptAuthority()
339	anyuser := testutils.TestAddress("anyuser")
340
341	// Test that any action is authorized with args
342	executed := false
343	testArgs := []any{"arg1", 42, "arg3"}
344	err := auth.Authorize(anyuser, "test_action", func() error {
345		executed = true
346		return nil
347	}, testArgs...)
348
349	uassert.True(t, err == nil, "auto-accept should not return error")
350	uassert.True(t, executed, "action should have been executed")
351}
352
353func TestMemberAuthorityMultipleMembers(cur realm, t *testing.T) {
354	alice := testutils.TestAddress("alice")
355	bob := testutils.TestAddress("bob")
356	carol := testutils.TestAddress("carol")
357
358	// Create authority with multiple members
359	auth := NewMemberAuthority(alice, bob)
360
361	// Test that both members can execute actions
362	for _, member := range []address{alice, bob} {
363		err := auth.Authorize(member, "test_action", func() error {
364			return nil
365		})
366		uassert.True(t, err == nil, "member should be authorized")
367	}
368
369	// Test that non-member cannot execute
370	err := auth.Authorize(carol, "test_action", func() error {
371		return nil
372	})
373	uassert.True(t, err != nil, "non-member should not be authorized")
374
375	// Test Tree() functionality
376	tree := auth.Tree()
377	uassert.True(t, tree.Size() == 2, "tree should have 2 members")
378
379	// Verify both members are in the tree
380	found := make(map[address]bool)
381	tree.Iterate("", "", func(key string, _ any) bool {
382		found[address(key)] = true
383		return false
384	})
385	uassert.True(t, found[alice], "alice should be in the tree")
386	uassert.True(t, found[bob], "bob should be in the tree")
387	uassert.False(t, found[carol], "carol should not be in the tree")
388
389	// Test read-only nature of the tree
390	defer func() {
391		r := recover()
392		uassert.True(t, r != nil, "modifying read-only tree should panic")
393	}()
394	tree.Set(string(carol), nil) // This should panic
395}
396
397func TestAuthorizerCurrentNeverNil(cur realm, t *testing.T) {
398	auth := NewWithMembers(cur.Address())
399
400	// Authority should never be nil after initialization
401	uassert.True(t, auth.Authority() != nil, "current authority should not be nil")
402
403	// Authority should not be nil after transfer
404	var err error
405	func(cur realm) { err = auth.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
406	uassert.True(t, err == nil, "transfer should succeed")
407	uassert.True(t, auth.Authority() != nil, "current authority should not be nil after transfer")
408}
409
410func TestContractAuthorityValidation(cur realm, t *testing.T) {
411	/*
412		// Test empty path - should panic
413		panicked := false
414		func() {
415			defer func() {
416				if r := recover(); r != nil {
417					panicked = true
418				}
419			}()
420			NewContractAuthority("", nil)
421		}()
422		uassert.True(t, panicked, "expected panic for empty path")
423	*/
424
425	// Test nil handler - should return error on Authorize
426	auth := NewContractAuthority("gno.land/r/test", nil)
427	code := testing.NewCodeRealm("gno.land/r/test").Address()
428	err := auth.Authorize(code, "test", func() error {
429		return nil
430	})
431	uassert.True(t, err != nil, "nil handler authority should fail to authorize")
432
433	// Test valid configuration
434	handler := func(title string, action PrivilegedAction) error {
435		return nil
436	}
437	contractAuth := NewContractAuthority("gno.land/r/test", handler)
438	err = contractAuth.Authorize(code, "test", func() error {
439		return nil
440	})
441	uassert.True(t, err == nil, "valid contract authority should authorize successfully")
442}
443
444func TestAuthorizerString(cur realm, t *testing.T) {
445	auth := NewWithMembers(cur.Address())
446	addr := cur.Address()
447
448	// Test initial string representation
449	str := auth.String()
450	uassert.Equal(t, str, "member_authority["+string(addr)+"]")
451
452	// Test string after transfer — caller is the current member (cur).
453	autoAuth := NewAutoAcceptAuthority()
454	var err error
455	func(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))
456	uassert.True(t, err == nil, "transfer should succeed")
457	str = auth.String()
458	uassert.Equal(t, str, "auto_accept_authority")
459
460	// Test custom authority — auto-accept lets anyone transfer.
461	customAuth := &mockAuthority{}
462	func(cur realm) { err = auth.Transfer(0, cur, customAuth) }(cross(cur))
463	uassert.True(t, err == nil, "transfer should succeed")
464	str = auth.String()
465	uassert.Equal(t, str, "custom_authority[mock]")
466}
467
468type mockAuthority struct{}
469
470func (c mockAuthority) String() string { return "mock" }
471func (a mockAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
472	// autoaccept
473	return action()
474}
475
476func TestAuthorityString(cur realm, t *testing.T) {
477	alice := testutils.TestAddress("alice")
478
479	// MemberAuthority
480	memberAuth := NewMemberAuthority(alice)
481	memberStr := memberAuth.String()
482	expectedMemberStr := "member_authority[g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh]"
483	uassert.Equal(t, memberStr, expectedMemberStr)
484
485	// ContractAuthority
486	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { return nil })
487	contractStr := contractAuth.String()
488	expectedContractStr := "contract_authority[contract=gno.land/r/test]"
489	uassert.Equal(t, contractStr, expectedContractStr)
490
491	// AutoAcceptAuthority
492	autoAuth := NewAutoAcceptAuthority()
493	autoStr := autoAuth.String()
494	expectedAutoStr := "auto_accept_authority"
495	uassert.Equal(t, autoStr, expectedAutoStr)
496
497	// DroppedAuthority
498	droppedAuth := NewDroppedAuthority()
499	droppedStr := droppedAuth.String()
500	expectedDroppedStr := "dropped_authority"
501	uassert.Equal(t, droppedStr, expectedDroppedStr)
502}
503
504func TestContractAuthorityUnauthorizedCaller(cur realm, t *testing.T) {
505	contractPath := "gno.land/r/testcontract"
506	contractAddr := chain.PackageAddress(contractPath)
507	unauthorizedAddr := testutils.TestAddress("unauthorized")
508
509	// Handler that checks the caller before proceeding
510	handlerExecutedCorrectly := false // Tracks if handler logic ran correctly
511	handlerErrorMsg := "handler: caller is not the contract"
512	contractHandler := func(title string, action PrivilegedAction) error {
513		caller := unsafe.CurrentRealm().Address()
514		if caller != contractAddr {
515			return errors.New(handlerErrorMsg)
516		}
517		// Only execute action and mark success if caller is correct
518		handlerExecutedCorrectly = true
519		return action()
520	}
521
522	contractAuth := NewContractAuthority(contractPath, contractHandler)
523	authorizer := NewWithAuthority(contractAuth) // Start with ContractAuthority
524
525	actionExecuted := false
526	privilegedAction := func() error {
527		actionExecuted = true
528		return nil
529	}
530
531	// 1. Attempt action from unauthorized user
532	testing.SetRealm(testing.NewUserRealm(unauthorizedAddr))
533	err := authorizer.DoByCurrent(0, cur, "test_action_unauthorized", privilegedAction)
534
535	// Assertions for unauthorized call
536	uassert.Error(t, err, "DoByCurrent should return an error for unauthorized caller")
537	uassert.ErrorContains(t, err, handlerErrorMsg, "Error should originate from the handler check")
538	uassert.False(t, handlerExecutedCorrectly, "Handler should not have executed successfully for unauthorized caller")
539	uassert.False(t, actionExecuted, "Privileged action should not have executed for unauthorized caller")
540
541	// 2. Attempt action from the correct contract
542	handlerExecutedCorrectly = false // Reset flag
543	actionExecuted = false           // Reset flag
544	testing.SetRealm(testing.NewCodeRealm(contractPath))
545	err = authorizer.DoByCurrent(0, cur, "test_action_authorized", privilegedAction)
546
547	// Assertions for authorized call
548	uassert.NoError(t, err, "DoByCurrent should succeed for authorized contract caller")
549	uassert.True(t, handlerExecutedCorrectly, "Handler should have executed successfully for authorized caller")
550	uassert.True(t, actionExecuted, "Privileged action should have executed for authorized caller")
551}
552
553// TestAuthorizerDoByPrevious verifies the "calling realm authorizes"
554// pattern: a function (the inner crossing closure) invokes
555// DoByPrevious so the authority check sees cur.Previous() — the realm
556// that crossed into it — not the function's own realm.
557//
558// Each scenario crosses into the inner closure via cross(cur) after
559// SetRealm — inside the closure, cur is the fresh live cur and
560// cur.Previous() is the SetRealm'd outer realm. This is the only way
561// to exercise DoByPrevious correctly under the IsCurrent guard, which
562// rejects synthetic realm values (testing.MakeRealm) and stored
563// stale captures.
564func TestAuthorizerDoByPrevious(cur realm, t *testing.T) {
565	alice := testutils.TestAddress("alice")
566	bob := testutils.TestAddress("bob")
567
568	auth := NewWithMembers(alice)
569
570	// alice (member) crosses in: cur.Previous() == alice inside the inner closure.
571	testing.SetRealm(testing.NewUserRealm(alice))
572	executed := false
573	args := []any{"test_arg", 123}
574	func(cur realm) {
575		err := auth.DoByPrevious(0, cur, "test_action", func() error {
576			executed = true
577			return nil
578		}, args...)
579		uassert.NoError(t, err, "expected no error")
580		uassert.True(t, executed, "action should have been executed")
581	}(cross(cur))
582
583	expectedErr := errors.New("test error")
584	func(cur realm) {
585		err := auth.DoByPrevious(0, cur, "test_action", func() error {
586			return expectedErr
587		})
588		uassert.ErrorContains(t, err, expectedErr.Error(), "expected error")
589	}(cross(cur))
590
591	// bob (not a member) crosses in: Authorize must reject.
592	testing.SetRealm(testing.NewUserRealm(bob))
593	executed = false
594	func(cur realm) {
595		err := auth.DoByPrevious(0, cur, "test_action", func() error {
596			executed = true
597			return nil
598		}, "unauthorized_arg")
599		uassert.ErrorContains(t, err, "unauthorized", "expected error")
600		uassert.False(t, executed, "action should not have been executed")
601	}(cross(cur))
602}