Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

authz_test.gno

36.83 Kb · 963 lines
  1package authz
  2
  3import (
  4	"chain"
  5	"errors"
  6	"strings"
  7	"testing"
  8
  9	"gno.land/p/nt/testutils/v0"
 10	"gno.land/p/nt/uassert/v0"
 11)
 12
 13func TestNewWithCurrent(cur realm, t *testing.T) {
 14	alice := testutils.TestAddress("alice")
 15	testing.SetRealm(testing.NewUserRealm(alice))
 16
 17	auth := NewWithMembers(cur.Address())
 18
 19	// Check that the current authority is a MemberAuthority
 20	memberAuth, ok := auth.Authority().(*MemberAuthority)
 21	uassert.True(t, ok, "expected MemberAuthority")
 22
 23	// Check that the caller is a member
 24	uassert.True(t, memberAuth.Has(alice), "caller should be a member")
 25
 26	// Check string representation
 27	uassert.True(t, strings.Contains(auth.String(), alice.String()))
 28}
 29
 30func TestNewWithAuthority(cur realm, t *testing.T) {
 31	alice := testutils.TestAddress("alice")
 32	memberAuth := NewMemberAuthority(alice)
 33
 34	auth := NewWithAuthority(memberAuth)
 35
 36	// Check that the current authority is the one we provided
 37	uassert.True(t, auth.Authority() == memberAuth, "expected provided authority")
 38}
 39
 40func TestAuthorizerAuthorize(cur realm, t *testing.T) {
 41	alice := testutils.TestAddress("alice")
 42	testing.SetRealm(testing.NewUserRealm(alice))
 43
 44	auth := NewWithMembers(cur.Address())
 45
 46	// Test successful action with args
 47	executed := false
 48	args := []any{"test_arg", 123}
 49	err := auth.DoByCurrent(0, cur, "test_action", func() error {
 50		executed = true
 51		return nil
 52	}, args...)
 53
 54	uassert.True(t, err == nil, "expected no error")
 55	uassert.True(t, executed, "action should have been executed")
 56
 57	// Test unauthorized action with args
 58	testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("bob")))
 59
 60	executed = false
 61	err = auth.DoByCurrent(0, cur, "test_action", func() error {
 62		executed = true
 63		return nil
 64	}, "unauthorized_arg")
 65
 66	uassert.True(t, err != nil, "expected error")
 67	uassert.False(t, executed, "action should not have been executed")
 68
 69	// Test action returning error
 70	testing.SetRealm(testing.NewUserRealm(alice))
 71	expectedErr := errors.New("test error")
 72
 73	err = auth.DoByCurrent(0, cur, "test_action", func() error {
 74		return expectedErr
 75	})
 76
 77	uassert.True(t, err == expectedErr, "expected specific error")
 78}
 79
 80func TestAuthorizerTransfer(cur realm, t *testing.T) {
 81	alice := testutils.TestAddress("alice")
 82	testing.SetRealm(testing.NewUserRealm(alice))
 83
 84	auth := NewWithMembers(cur.Address())
 85
 86	// Test transfer to new member authority
 87	bob := testutils.TestAddress("bob")
 88	newAuth := NewMemberAuthority(bob)
 89
 90	var err error
 91	func(cur realm) { err = auth.Transfer(0, cur, newAuth) }(cross(cur))
 92	uassert.True(t, err == nil, "expected no error")
 93	uassert.True(t, auth.Authority() == newAuth, "expected new authority")
 94
 95	// Test unauthorized transfer: principal is not a member of newAuth.
 96	carol := testutils.TestAddress("carol")
 97	testing.SetRealm(testing.NewUserRealm(carol))
 98
 99	func(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))
100	uassert.True(t, err != nil, "expected error")
101
102	// Test transfer to contract authority — bob is the current authority.
103	testing.SetRealm(testing.NewUserRealm(bob))
104	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
105		return action()
106	})
107
108	func(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))
109	uassert.True(t, err == nil, "expected no error")
110	uassert.True(t, auth.Authority() == contractAuth, "expected contract authority")
111}
112
113func TestAuthorizerTransferChain(cur realm, t *testing.T) {
114	alice := testutils.TestAddress("alice")
115	testing.SetRealm(testing.NewUserRealm(alice))
116
117	// Create a chain of transfers
118	auth := NewWithMembers(cur.Address())
119
120	// First transfer to a new member authority
121	bob := testutils.TestAddress("bob")
122	memberAuth := NewMemberAuthority(bob)
123
124	var err error
125	func(cur realm) { err = auth.Transfer(0, cur, memberAuth) }(cross(cur))
126	uassert.True(t, err == nil, "unexpected error in first transfer")
127
128	// Then transfer to a contract authority — bob is now the authority.
129	testing.SetRealm(testing.NewUserRealm(bob))
130	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
131		return action()
132	})
133	func(cur realm) { err = auth.Transfer(0, cur, contractAuth) }(cross(cur))
134	uassert.True(t, err == nil, "unexpected error in second transfer")
135
136	// Finally transfer to an auto-accept authority. The authority is now
137	// the contract authority, whose default proposer is the contract
138	// itself, so the transfer's caller —
139	// cur.Previous() — must be the contract. Set the outer realm to the
140	// contract, then cross into a closure so cur.Previous() resolves to
141	// it.
142	autoAuth := NewAutoAcceptAuthority()
143	testing.SetRealm(testing.NewCodeRealm("gno.land/r/test"))
144	func(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))
145	uassert.True(t, err == nil, "unexpected error in final transfer")
146	uassert.True(t, auth.Authority() == autoAuth, "expected auto-accept authority")
147}
148
149func TestAuthorizerTransferUnauthorizedRejected(cur realm, t *testing.T) {
150	// Regression for the address-parameter forgery: previously Transfer
151	// took a caller-supplied `caller address` that an attacker realm could
152	// set to the real owner. After the IsCurrent + rlm.Previous() fix, the
153	// principal is the captured cur's previous and cannot be forged.
154	admin := testutils.TestAddress("admin")
155	attacker := testutils.TestAddress("attacker")
156
157	testing.SetRealm(testing.NewUserRealm(admin))
158	auth := NewWithMembers(cur.Address()) // admin is the initial authority
159
160	initialAuth, ok := auth.Authority().(*MemberAuthority)
161	uassert.True(t, ok)
162	uassert.True(t, initialAuth.Has(admin))
163	uassert.False(t, initialAuth.Has(attacker))
164
165	// Attacker context: cur.Previous() inside the closure will be attacker.
166	testing.SetRealm(testing.NewUserRealm(attacker))
167	attackerAuth := NewMemberAuthority(attacker)
168	var err error
169	func(cur realm) { err = auth.Transfer(0, cur, attackerAuth) }(cross(cur))
170
171	uassert.True(t, err != nil, "attacker transfer must be rejected")
172
173	// Authority unchanged: still the initial admin-only MemberAuthority.
174	finalAuth, ok := auth.Authority().(*MemberAuthority)
175	uassert.True(t, ok)
176	uassert.True(t, finalAuth == initialAuth, "authority must not have changed")
177	uassert.True(t, finalAuth.Has(admin))
178	uassert.False(t, finalAuth.Has(attacker))
179}
180
181func TestAuthorizerWithDroppedAuthority(cur realm, t *testing.T) {
182	alice := testutils.TestAddress("alice")
183	testing.SetRealm(testing.NewUserRealm(alice))
184
185	auth := NewWithMembers(cur.Address())
186
187	// Transfer to dropped authority
188	var err error
189	func(cur realm) { err = auth.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))
190	uassert.True(t, err == nil, "expected no error")
191
192	// Try to execute action
193	err = auth.DoByCurrent(0, cur, "test_action", func() error {
194		return nil
195	})
196	uassert.True(t, err != nil, "expected error from dropped authority")
197
198	// Try to transfer again
199	func(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))
200	uassert.True(t, err != nil, "expected error when transferring from dropped authority")
201}
202
203func TestContractAuthorityHandlerExecutionOnce(cur realm, t *testing.T) {
204	attempts := 0
205	executed := 0
206
207	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
208		// Try to execute the action twice in the same handler
209		if err := action(); err != nil {
210			return err
211		}
212		attempts++
213
214		// Second execution should fail
215		if err := action(); err != nil {
216			return err
217		}
218		attempts++
219		return nil
220	})
221
222	// Set caller to contract address
223	codeRealm := testing.NewCodeRealm("gno.land/r/test")
224	testing.SetRealm(codeRealm)
225	code := codeRealm.Address()
226
227	testArgs := []any{"proposal_id", 42, "metadata", map[string]string{"key": "value"}}
228	err := contractAuth.Authorize(code, "test_action", func() error {
229		executed++
230		return nil
231	}, testArgs...)
232
233	uassert.True(t, err == nil, "handler execution should succeed")
234	uassert.True(t, attempts == 2, "handler should have attempted execution twice")
235	uassert.True(t, executed == 1, "handler should have executed once")
236}
237
238func TestContractAuthorityExecutionTwice(cur realm, t *testing.T) {
239	executed := 0
240
241	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
242		return action()
243	})
244
245	// Set caller to contract address
246	codeRealm := testing.NewCodeRealm("gno.land/r/test")
247	testing.SetRealm(codeRealm)
248	code := codeRealm.Address()
249	testArgs := []any{"proposal_id", 42, "metadata", map[string]string{"key": "value"}}
250
251	err := contractAuth.Authorize(code, "test_action", func() error {
252		executed++
253		return nil
254	}, testArgs...)
255
256	uassert.True(t, err == nil, "handler execution should succeed")
257	uassert.True(t, executed == 1, "handler should have executed once")
258
259	// A new action, even with the same title, should be executed
260	err = contractAuth.Authorize(code, "test_action", func() error {
261		executed++
262		return nil
263	}, testArgs...)
264
265	uassert.True(t, err == nil, "handler execution should succeed")
266	uassert.True(t, executed == 2, "handler should have executed twice")
267}
268
269func TestContractAuthorityWithProposer(cur realm, t *testing.T) {
270	alice := testutils.TestAddress("alice")
271	memberAuth := NewMemberAuthority(alice)
272
273	handlerCalled := false
274	actionExecuted := false
275
276	contractAuth := NewRestrictedContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {
277		handlerCalled = true
278		// Set caller to contract address before executing action
279		testing.SetRealm(testing.NewCodeRealm("gno.land/r/test"))
280		return action()
281	}, memberAuth)
282
283	// Test authorized member
284	testArgs := []any{"proposal_metadata", "test value"}
285	err := contractAuth.Authorize(alice, "test_action", func() error {
286		actionExecuted = true
287		return nil
288	}, testArgs...)
289
290	uassert.True(t, err == nil, "authorized member should be able to propose")
291	uassert.True(t, handlerCalled, "contract handler should be called")
292	uassert.True(t, actionExecuted, "action should be executed")
293
294	// Reset flags for unauthorized test
295	handlerCalled = false
296	actionExecuted = false
297
298	// Test unauthorized proposer
299	bob := testutils.TestAddress("bob")
300	err = contractAuth.Authorize(bob, "test_action", func() error {
301		actionExecuted = true
302		return nil
303	}, testArgs...)
304
305	uassert.True(t, err != nil, "unauthorized member should not be able to propose")
306	uassert.False(t, handlerCalled, "contract handler should not be called for unauthorized proposer")
307	uassert.False(t, actionExecuted, "action should not be executed for unauthorized proposer")
308}
309
310func TestAutoAcceptAuthority(cur realm, t *testing.T) {
311	alice := testutils.TestAddress("alice")
312	auth := NewAutoAcceptAuthority()
313
314	// Test that any action is authorized
315	executed := false
316	err := auth.Authorize(alice, "test_action", func() error {
317		executed = true
318		return nil
319	})
320
321	uassert.True(t, err == nil, "auto-accept should not return error")
322	uassert.True(t, executed, "action should have been executed")
323
324	// Test with different caller
325	random := testutils.TestAddress("random")
326	executed = false
327	err = auth.Authorize(random, "test_action", func() error {
328		executed = true
329		return nil
330	})
331
332	uassert.True(t, err == nil, "auto-accept should not care about caller")
333	uassert.True(t, executed, "action should have been executed")
334}
335
336func TestAutoAcceptAuthorityWithArgs(cur realm, t *testing.T) {
337	auth := NewAutoAcceptAuthority()
338	anyuser := testutils.TestAddress("anyuser")
339
340	// Test that any action is authorized with args
341	executed := false
342	testArgs := []any{"arg1", 42, "arg3"}
343	err := auth.Authorize(anyuser, "test_action", func() error {
344		executed = true
345		return nil
346	}, testArgs...)
347
348	uassert.True(t, err == nil, "auto-accept should not return error")
349	uassert.True(t, executed, "action should have been executed")
350}
351
352func TestMemberAuthorityMultipleMembers(cur realm, t *testing.T) {
353	alice := testutils.TestAddress("alice")
354	bob := testutils.TestAddress("bob")
355	carol := testutils.TestAddress("carol")
356
357	// Create authority with multiple members
358	auth := NewMemberAuthority(alice, bob)
359
360	// Test that both members can execute actions
361	for _, member := range []address{alice, bob} {
362		err := auth.Authorize(member, "test_action", func() error {
363			return nil
364		})
365		uassert.True(t, err == nil, "member should be authorized")
366	}
367
368	// Test that non-member cannot execute
369	err := auth.Authorize(carol, "test_action", func() error {
370		return nil
371	})
372	uassert.True(t, err != nil, "non-member should not be authorized")
373
374	// Test Tree() functionality
375	tree := auth.Tree()
376	uassert.True(t, tree.Size() == 2, "tree should have 2 members")
377
378	// Verify both members are in the tree
379	found := make(map[address]bool)
380	tree.Iterate("", "", func(key string, _ any) bool {
381		found[address(key)] = true
382		return false
383	})
384	uassert.True(t, found[alice], "alice should be in the tree")
385	uassert.True(t, found[bob], "bob should be in the tree")
386	uassert.False(t, found[carol], "carol should not be in the tree")
387
388	// Test read-only nature of the tree
389	defer func() {
390		r := recover()
391		uassert.True(t, r != nil, "modifying read-only tree should panic")
392	}()
393	tree.Set(string(carol), nil) // This should panic
394}
395
396func TestAuthorizerCurrentNeverNil(cur realm, t *testing.T) {
397	auth := NewWithMembers(cur.Address())
398
399	// Authority should never be nil after initialization
400	uassert.True(t, auth.Authority() != nil, "current authority should not be nil")
401
402	// Authority should not be nil after transfer
403	var err error
404	func(cur realm) { err = auth.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
405	uassert.True(t, err == nil, "transfer should succeed")
406	uassert.True(t, auth.Authority() != nil, "current authority should not be nil after transfer")
407}
408
409func TestContractAuthorityValidation(cur realm, t *testing.T) {
410	handler := func(title string, action PrivilegedAction) error {
411		return nil
412	}
413
414	// Empty path panics (consistent with NewRestrictedContractAuthority).
415	uassert.PanicsWithMessage(t, cur, "contract path cannot be empty", func() {
416		NewContractAuthority("", handler)
417	})
418
419	// A nil handler panics at CONSTRUCTION rather than being tolerated and
420	// surfaced at Authorize time. Tolerating it produced a permanent brick:
421	// see TestNilHandlerWouldBeUnrotatable below.
422	uassert.PanicsWithMessage(t, cur, "contract handler cannot be nil", func() {
423		NewContractAuthority("gno.land/r/test", nil)
424	})
425
426	// The Authorize-time guard stays for a zero-value ContractAuthority,
427	// which the constructors cannot produce but a struct literal can.
428	code := testing.NewCodeRealm("gno.land/r/test").Address()
429	zero := &ContractAuthority{contractPath: "gno.land/r/test", contractAddr: code}
430	err := zero.Authorize(code, "test", func() error {
431		return nil
432	})
433	uassert.True(t, err != nil, "nil handler authority should fail to authorize")
434
435	// Test valid configuration
436	contractAuth := NewContractAuthority("gno.land/r/test", handler)
437	err = contractAuth.Authorize(code, "test", func() error {
438		return nil
439	})
440	uassert.True(t, err == nil, "valid contract authority should authorize successfully")
441}
442
443func TestAuthorizerString(cur realm, t *testing.T) {
444	auth := NewWithMembers(cur.Address())
445	addr := cur.Address()
446
447	// Test initial string representation
448	str := auth.String()
449	uassert.Equal(t, str, "member_authority["+string(addr)+"]")
450
451	// Test string after transfer — caller is the current member (cur).
452	autoAuth := NewAutoAcceptAuthority()
453	var err error
454	func(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))
455	uassert.True(t, err == nil, "transfer should succeed")
456	str = auth.String()
457	uassert.Equal(t, str, "auto_accept_authority")
458
459	// Test custom authority — auto-accept lets anyone transfer.
460	customAuth := &mockAuthority{}
461	func(cur realm) { err = auth.Transfer(0, cur, customAuth) }(cross(cur))
462	uassert.True(t, err == nil, "transfer should succeed")
463	str = auth.String()
464	uassert.Equal(t, str, "custom_authority[mock]")
465}
466
467type mockAuthority struct{}
468
469func (c mockAuthority) String() string { return "mock" }
470func (a mockAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
471	// autoaccept
472	return action()
473}
474
475func TestAuthorityString(cur realm, t *testing.T) {
476	alice := testutils.TestAddress("alice")
477
478	// MemberAuthority
479	memberAuth := NewMemberAuthority(alice)
480	memberStr := memberAuth.String()
481	expectedMemberStr := "member_authority[g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh]"
482	uassert.Equal(t, memberStr, expectedMemberStr)
483
484	// ContractAuthority — the proposer is rendered, not just the path.
485	// Without it the two constructors below are indistinguishable, which
486	// is what made consumer-level assertions on this string blind to an
487	// authority being swapped for a wide-open one.
488	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { return nil })
489	contractStr := contractAuth.String()
490	expectedContractStr := "contract_authority[contract=gno.land/r/test,proposer=contract-identity]"
491	uassert.Equal(t, contractStr, expectedContractStr)
492
493	// Same path, same handler, explicit open proposer — must render
494	// differently from the gated default above.
495	openAuth := NewRestrictedContractAuthority(
496		"gno.land/r/test",
497		func(title string, action PrivilegedAction) error { return nil },
498		NewAutoAcceptAuthority(),
499	)
500	uassert.Equal(t,
501		"contract_authority[contract=gno.land/r/test,proposer=auto_accept_authority]",
502		openAuth.String())
503	uassert.NotEqual(t, contractStr, openAuth.String())
504
505	// AutoAcceptAuthority
506	autoAuth := NewAutoAcceptAuthority()
507	autoStr := autoAuth.String()
508	expectedAutoStr := "auto_accept_authority"
509	uassert.Equal(t, autoStr, expectedAutoStr)
510
511	// DroppedAuthority
512	droppedAuth := NewDroppedAuthority()
513	droppedStr := droppedAuth.String()
514	expectedDroppedStr := "dropped_authority"
515	uassert.Equal(t, droppedStr, expectedDroppedStr)
516}
517
518// TestContractAuthorityUnauthorizedCaller verifies the
519// fix: a ContractAuthority's default proposer is the contract itself, so
520// any other caller is rejected UPSTREAM (at the proposer) and the handler
521// and privileged action never run. Previously the default proposer was
522// AutoAcceptAuthority, and the only guard was a handler-side
523// `unsafe.CurrentRealm() == contractAddr` check — which was bypassable
524// and which many consumers (e.g. r/gnops/valopers) omitted entirely,
525// leaving no caller check at all.
526func TestContractAuthorityUnauthorizedCaller(cur realm, t *testing.T) {
527	contractPath := "gno.land/r/testcontract"
528	contractAddr := chain.PackageAddress(contractPath)
529	unauthorizedAddr := testutils.TestAddress("unauthorized")
530
531	// A permissive handler that simply runs the action — the realistic
532	// shape a consumer registers. The package, not the handler, must be
533	// the one that rejects an unauthorized caller.
534	handlerExecuted := false
535	contractHandler := func(title string, action PrivilegedAction) error {
536		handlerExecuted = true
537		return action()
538	}
539	contractAuth := NewContractAuthority(contractPath, contractHandler)
540
541	actionExecuted := false
542	privilegedAction := func() error {
543		actionExecuted = true
544		return nil
545	}
546
547	// 1. Unauthorized caller: rejected by the contract-identity proposer
548	// before the handler or the action can run.
549	err := contractAuth.Authorize(unauthorizedAddr, "test_action", privilegedAction)
550	uassert.Error(t, err, "unauthorized caller must be rejected")
551	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
552	uassert.False(t, handlerExecuted, "handler must not run for an unauthorized caller")
553	uassert.False(t, actionExecuted, "privileged action must not run for an unauthorized caller")
554
555	// 2. The contract itself is authorized.
556	err = contractAuth.Authorize(contractAddr, "test_action", privilegedAction)
557	uassert.NoError(t, err, "the contract itself must be authorized")
558	uassert.True(t, handlerExecuted, "handler must run when the contract is the caller")
559	uassert.True(t, actionExecuted, "privileged action must run when the contract is the caller")
560}
561
562// TestAuthorizerDoByPrevious verifies the "calling realm authorizes"
563// pattern: a function (the inner crossing closure) invokes
564// DoByPrevious so the authority check sees cur.Previous() — the realm
565// that crossed into it — not the function's own realm.
566//
567// Each scenario crosses into the inner closure via cross(cur) after
568// SetRealm — inside the closure, cur is the fresh live cur and
569// cur.Previous() is the SetRealm'd outer realm. This is the only way
570// to exercise DoByPrevious correctly under the IsCurrent guard, which
571// rejects synthetic realm values (testing.MakeRealm) and stored
572// stale captures.
573func TestAuthorizerDoByPrevious(cur realm, t *testing.T) {
574	alice := testutils.TestAddress("alice")
575	bob := testutils.TestAddress("bob")
576
577	auth := NewWithMembers(alice)
578
579	// alice (member) crosses in: cur.Previous() == alice inside the inner closure.
580	testing.SetRealm(testing.NewUserRealm(alice))
581	executed := false
582	args := []any{"test_arg", 123}
583	func(cur realm) {
584		err := auth.DoByPrevious(0, cur, "test_action", func() error {
585			executed = true
586			return nil
587		}, args...)
588		uassert.NoError(t, err, "expected no error")
589		uassert.True(t, executed, "action should have been executed")
590	}(cross(cur))
591
592	expectedErr := errors.New("test error")
593	func(cur realm) {
594		err := auth.DoByPrevious(0, cur, "test_action", func() error {
595			return expectedErr
596		})
597		uassert.ErrorContains(t, err, expectedErr.Error(), "expected error")
598	}(cross(cur))
599
600	// bob (not a member) crosses in: Authorize must reject.
601	testing.SetRealm(testing.NewUserRealm(bob))
602	executed = false
603	func(cur realm) {
604		err := auth.DoByPrevious(0, cur, "test_action", func() error {
605			executed = true
606			return nil
607		}, "unauthorized_arg")
608		uassert.ErrorContains(t, err, "unauthorized", "expected error")
609		uassert.False(t, executed, "action should not have been executed")
610	}(cross(cur))
611}
612
613// ---------------------------------------------------------------------------
614// Contract-identity gate: regression suite.
615//
616// A plain NewContractAuthority now defaults its proposer to the contract
617// ITSELF (MemberAuthority of the contract's own package address) instead of
618// AutoAcceptAuthority. These tests pin the security contract: only the
619// contract may drive privileged actions, an external caller can neither
620// drive nor Transfer the authority, the contract can still rotate its own
621// authority, and the escape hatch for a genuinely-open proposer still
622// exists.
623// ---------------------------------------------------------------------------
624
625// The default proposer accepts the contract itself and rejects everyone
626// else, before the handler or action can run.
627func TestDefaultProposerIsContractOnly(cur realm, t *testing.T) {
628	const path = "gno.land/r/defcontract"
629	contractAddr := chain.PackageAddress(path)
630
631	ran := 0
632	ca := NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
633		return action()
634	})
635
636	// The contract itself: authorized.
637	err := ca.Authorize(contractAddr, "action", func() error { ran++; return nil })
638	uassert.NoError(t, err, "the contract itself must be authorized")
639
640	// Anyone else: rejected at the proposer, action never runs.
641	outsider := testutils.TestAddress("outsider")
642	err = ca.Authorize(outsider, "action", func() error { ran++; return nil })
643	uassert.Error(t, err, "a non-contract caller must be rejected")
644	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
645
646	uassert.Equal(t, 1, ran, "only the contract's action must have run")
647}
648
649// An external realm that holds another realm's Authorizer cannot Transfer
650// it. The caller derived under IsCurrent is that external realm, not the
651// contract, so the rotation is rejected and the authority is left intact.
652func TestExternalTransferRejected(cur realm, t *testing.T) {
653	authorizer := NewWithAuthority(
654		NewContractAuthority("gno.land/r/victim", func(_ string, action PrivilegedAction) error {
655			return action()
656		}),
657	)
658
659	attacker := testutils.TestAddress("attacker")
660	testing.SetRealm(testing.NewUserRealm(attacker))
661
662	var err error
663	func(cur realm) { err = authorizer.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))
664
665	uassert.Error(t, err, "external Transfer must be rejected")
666	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
667
668	_, ok := authorizer.Authority().(*ContractAuthority)
669	uassert.True(t, ok, "authority must be unchanged after a rejected rotation")
670}
671
672// Same shape via DoByPrevious (a privileged action instead of a
673// Transfer): an external caller is rejected and the action never runs.
674func TestExternalDoByPreviousRejected(cur realm, t *testing.T) {
675	authorizer := NewWithAuthority(
676		NewContractAuthority("gno.land/r/victim2", func(_ string, action PrivilegedAction) error {
677			return action()
678		}),
679	)
680
681	ran := false
682	testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("randomuser")))
683	var err error
684	func(cur realm) {
685		err = authorizer.DoByPrevious(0, cur, "privileged", func() error { ran = true; return nil })
686	}(cross(cur))
687
688	uassert.Error(t, err, "external DoByPrevious must be rejected")
689	uassert.False(t, ran, "privileged action must not run for an external caller")
690}
691
692// The contract-identity model does NOT lock out legitimate governance: the
693// contract can still rotate (Transfer) its own authority when the transfer
694// is driven with the contract as the previous realm (caller == contractAddr).
695func TestContractCanRotateItsOwnAuthority(cur realm, t *testing.T) {
696	const path = "gno.land/r/selfgov"
697	authorizer := NewWithAuthority(
698		NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
699			return action()
700		}),
701	)
702
703	// Drive the transfer with the contract as the caller: set the outer
704	// realm to the contract, then cross into a closure so cur.Previous()
705	// resolves to the contract.
706	testing.SetRealm(testing.NewCodeRealm(path))
707	var err error
708	func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
709
710	uassert.NoError(t, err, "the contract itself must be able to rotate its authority")
711	_, ok := authorizer.Authority().(*AutoAcceptAuthority)
712	uassert.True(t, ok, "authority should have rotated to auto-accept")
713}
714
715// Boundary test: the raw Authority.Authorize takes `caller` as a plain
716// parameter, so a holder of the raw Authority can forge caller ==
717// contractAddr. This proves that even so, a forged-caller raw Authorize
718// only runs the action the caller itself supplies — it CANNOT mutate an
719// Authorizer's installed authority, because the Transfer mutation lives
720// only inside Authorizer.Transfer (which derives caller non-forgeably).
721// The lesson encoded here: consumers must reach a ContractAuthority through
722// the Authorizer wrapper and must not hand out the raw Authority.
723//
724// WHAT THIS TEST DOES AND DOES NOT FREEZE. Only the final assertion is a
725// security property. That raw Authorize ACCEPTS a forged caller is
726// current behaviour, not a guarantee — it is the consequence of `caller`
727// being a parameter on an exported interface method. If someone later
728// hardens that path so raw Authorize stops accepting a forged caller,
729// this test going red means the test is stale, NOT that the change is
730// wrong: delete the two intermediate assertions and keep the last one.
731// (Recorded because asserting NoError here reads like the forgeability
732// is wanted. It is tolerated, and only because of what follows.)
733func TestForgedCallerCannotTransfer(cur realm, t *testing.T) {
734	const path = "gno.land/r/boundary"
735	contractAddr := chain.PackageAddress(path)
736
737	authorizer := NewWithAuthority(
738		NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
739			return action()
740		}),
741	)
742
743	// Attacker obtains the raw Authority and forges the contract as caller.
744	attackerRan := false
745	err := authorizer.Authority().Authorize(contractAddr, "transfer_authority", func() error {
746		attackerRan = true
747		return nil
748	})
749
750	// Current behaviour, documented above — not a property to preserve.
751	uassert.NoError(t, err, "raw Authorize currently accepts a forged caller (see comment)")
752	uassert.True(t, attackerRan, "only the caller's own inert closure runs")
753
754	// THE property: the installed authority is UNCHANGED. Raw Authorize
755	// cannot perform a Transfer — the mutation closure is private to
756	// Authorizer.Transfer, which derives caller non-forgeably.
757	_, ok := authorizer.Authority().(*ContractAuthority)
758	uassert.True(t, ok, "raw Authorize must not be able to transfer the authority")
759}
760
761// Two contract authorities on different paths never cross-authorize.
762func TestContractPathIsolation(cur realm, t *testing.T) {
763	caB := NewContractAuthority("gno.land/r/pathb", func(_ string, action PrivilegedAction) error {
764		return action()
765	})
766	addrA := chain.PackageAddress("gno.land/r/patha")
767
768	ran := false
769	err := caB.Authorize(addrA, "action", func() error { ran = true; return nil })
770	uassert.Error(t, err, "contract A must not authorize contract B's authority")
771	uassert.False(t, ran, "action must not run across contract identities")
772}
773
774// Escape hatch: a consumer that genuinely wants open proposals can still
775// opt in via NewRestrictedContractAuthority with an AutoAcceptAuthority
776// proposer — restoring the pre-fix "anyone can propose" behavior explicitly.
777func TestRestrictedAutoAcceptEscapeHatch(cur realm, t *testing.T) {
778	ca := NewRestrictedContractAuthority(
779		"gno.land/r/open",
780		func(_ string, action PrivilegedAction) error { return action() },
781		NewAutoAcceptAuthority(),
782	)
783
784	ran := false
785	err := ca.Authorize(testutils.TestAddress("anyone"), "action", func() error { ran = true; return nil })
786	uassert.NoError(t, err, "an explicit AutoAccept proposer restores open proposals")
787	uassert.True(t, ran, "action must run under an explicit AutoAccept proposer")
788}
789
790// Why the nil handler is now rejected at construction rather than
791// tolerated: Authorize checks contractHandler == nil BEFORE consulting the
792// proposer, and Transfer routes through Authorize, so there is no rotation
793// path out. It is a permanent brick reachable by an ordinary deployer
794// typo -- the same bricked-governance failure mode, by accident.
795//
796// The zero value is used because the constructor no longer allows it.
797func TestNilHandlerWouldBeUnrotatable(cur realm, t *testing.T) {
798	const path = "gno.land/r/nilbrick"
799	addr := chain.PackageAddress(path)
800	authorizer := NewWithAuthority(&ContractAuthority{
801		contractPath: path,
802		contractAddr: addr,
803		// Both handler and proposer are nil: a struct literal is the only
804		// way to reach this, and Authorize fails closed on either.
805	})
806
807	// Drive the rotation as the contract itself -- the only principal the
808	// default proposer accepts. If this cannot escape, nobody can.
809	testing.SetRealm(testing.NewCodeRealm(path))
810	var err error
811	func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
812
813	uassert.Error(t, err, "a nil-handler authority must not be silently rotatable")
814	_, stuck := authorizer.Authority().(*ContractAuthority)
815	uassert.True(t, stuck, "nil-handler authority is unrotatable -- hence rejected at construction")
816}
817
818// A crossing closure declared HERE carries this package's identity, not
819// the contract path's -- testing.SetRealm does not change that, because
820// the VM mints a crossing frame's realm from the callee's declaring
821// package. So the DoByCurrent shape that Example_contractAuthority teaches
822// cannot be exercised from inside this package at all; it needs a real
823// realm. It is covered by filetests/z_contract_authority_shape_filetest.gno.
824//
825// The same property is why an exported crossing closure leaks a realm's
826// authority to any caller -- it is one mechanism seen from two
827// sides. This test pins the half that is observable here: an unrelated
828// package's frame is rejected.
829func TestForeignFrameCannotDriveContractAuthority(cur realm, t *testing.T) {
830	auth := NewWithAuthority(NewContractAuthority("gno.land/r/demo/dao", mockDAOHandler))
831
832	testing.SetRealm(testing.NewCodeRealm("gno.land/r/example"))
833	ran := false
834	var err error
835	func(cur realm) {
836		err = auth.DoByCurrent(0, cur, "update_params", func() error {
837			ran = true
838			return nil
839		})
840	}(cross(cur))
841
842	uassert.Error(t, err, "a frame from another package must not drive this authority")
843	uassert.False(t, ran, "the action must not run")
844}
845
846// A malformed contract path binds the authority to an address no realm can
847// ever present. Because Transfer routes through the same gate, such an
848// authority is also unrotatable: a permanent brick, and the same failure
849// mode the nil-handler panic exists to prevent. Rejected at construction.
850//
851// Pre-fix these all constructed happily; the AutoAccept default made the
852// typo harmless (and insecure), so nothing complained.
853func TestContractPathValidationRejectsMalformed(cur realm, t *testing.T) {
854	handler := func(_ string, action PrivilegedAction) error { return action() }
855
856	for _, path := range []string{
857		"gno.land/r/gov/dao ",  // trailing space
858		" gno.land/r/gov/dao",  // leading space
859		"gno.land/r/gov/dao\n", // trailing newline
860		"gno.land/r/gov dao",   // embedded space
861		" ",
862		"not a path",
863		"GNO.LAND/R/GOV/DAO", // uppercase: not a legal gno pkgpath
864		"gno.land//r/gov/dao",
865		"gno.land/r/gov/dao/",
866		"singlesegment",
867	} {
868		uassert.PanicsContains(t, cur, "contract path", func() {
869			NewContractAuthority(path, handler)
870		}, "malformed path must be rejected: "+path)
871		uassert.PanicsContains(t, cur, "contract path", func() {
872			NewRestrictedContractAuthority(path, handler, NewAutoAcceptAuthority())
873		}, "malformed path must be rejected by the restricted ctor too: "+path)
874	}
875
876	// Real paths still construct, including dashes, digits, underscores and
877	// version suffixes.
878	for _, path := range []string{
879		"gno.land/r/gov/dao",
880		"gno.land/r/gnops/valopers",
881		"gno.land/p/moul/authz/v0",
882		"gno.land/r/sys/validators/v0",
883		"gno.land/r/some-user/my_pkg2",
884	} {
885		uassert.NotPanics(t, cur, func() {
886			NewContractAuthority(path, handler)
887		}, "legitimate path must construct: "+path)
888	}
889}
890
891// spoofProposer is a wide-open Authority that LIES in String(), returning
892// the same text the canonical contract-identity default renders.
893type spoofProposer struct{}
894
895func (spoofProposer) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
896	return action()
897}
898
899func (spoofProposer) String() string { return "contract-identity" }
900
901// A foreign proposer must not be able to impersonate the gated default in
902// the rendered description. ContractAuthority.String() is promoted by this
903// package as the one surface distinguishing a gated authority from a
904// wide-open one, and consumers assert on it (r/gnops/valopers.Auth()). An
905// impl that chooses its own String() text defeated that: a fully permissive
906// authority rendered byte-identical to the default, so every string-based
907// configuration pin stayed green while the gate was gone.
908func TestSpoofedProposerCannotImpersonateContractIdentity(cur realm, t *testing.T) {
909	const path = "gno.land/r/spoofprobe"
910	handler := func(_ string, action PrivilegedAction) error { return action() }
911	outsider := chain.PackageAddress("gno.land/r/outsider")
912
913	gated := NewContractAuthority(path, handler)
914	spoof := NewRestrictedContractAuthority(path, handler, spoofProposer{})
915
916	// The behavioural difference the strings must reflect.
917	uassert.Error(t, gated.Authorize(outsider, "t", func() error { return nil }),
918		"the gated default must reject an outsider")
919	uassert.NoError(t, spoof.Authorize(outsider, "t", func() error { return nil }),
920		"the spoofing proposer is wide open -- that is the point of the test")
921
922	// ...and they do.
923	uassert.NotEqual(t, gated.String(), spoof.String(),
924		"a wide-open authority must not render identically to the gated default")
925	uassert.Equal(t,
926		"contract_authority[contract="+path+",proposer=contract-identity]",
927		gated.String())
928	uassert.Equal(t,
929		"contract_authority[contract="+path+",proposer=custom_authority[contract-identity]]",
930		spoof.String(),
931		"a non-canonical proposer must be wrapped, not trusted to name itself")
932
933	// Same guarantee through the Authorizer wrapper, which is the shape a
934	// consumer realm actually exposes.
935	uassert.NotEqual(t,
936		NewWithAuthority(gated).String(),
937		NewWithAuthority(spoof).String())
938}
939
940// An explicit proposer REPLACES the contract-identity gate; it does not add
941// to it. NewRestrictedContractAuthority(govdaoPath, h, member(alice)) means
942// "alice, and not GovDAO" -- contractAddr is never consulted. Pinned because
943// the rendered string reads like a conjunction and the old godoc said
944// "widen", so a consumer could reasonably have expected "both".
945func TestExplicitProposerReplacesIdentityGate(cur realm, t *testing.T) {
946	const path = "gno.land/r/gov/dao"
947	handler := func(_ string, action PrivilegedAction) error { return action() }
948	govdao := chain.PackageAddress(path)
949	alice := chain.PackageAddress("gno.land/r/alice")
950
951	gated := NewContractAuthority(path, handler)
952	replaced := NewRestrictedContractAuthority(path, handler, NewMemberAuthority(alice))
953
954	uassert.NoError(t, gated.Authorize(govdao, "x", func() error { return nil }),
955		"the default gate accepts the bound contract")
956	uassert.Error(t, gated.Authorize(alice, "x", func() error { return nil }),
957		"the default gate rejects everyone else")
958
959	uassert.Error(t, replaced.Authorize(govdao, "x", func() error { return nil }),
960		"an explicit proposer REPLACES the identity gate: govdao is no longer accepted")
961	uassert.NoError(t, replaced.Authorize(alice, "x", func() error { return nil }),
962		"only the explicit proposer's principal is accepted")
963}