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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

authz_test.gno

36.50 Kb · 948 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	// Verify membership through the public API: the v0 Tree() escape hatch
375	// is intentionally absent here, so the backing store never leaks.
376	uassert.True(t, auth.Has(alice), "alice should be a member")
377	uassert.True(t, auth.Has(bob), "bob should be a member")
378	uassert.False(t, auth.Has(carol), "carol should not be a member")
379}
380
381func TestAuthorizerCurrentNeverNil(cur realm, t *testing.T) {
382	auth := NewWithMembers(cur.Address())
383
384	// Authority should never be nil after initialization
385	uassert.True(t, auth.Authority() != nil, "current authority should not be nil")
386
387	// Authority should not be nil after transfer
388	var err error
389	func(cur realm) { err = auth.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
390	uassert.True(t, err == nil, "transfer should succeed")
391	uassert.True(t, auth.Authority() != nil, "current authority should not be nil after transfer")
392}
393
394func TestContractAuthorityValidation(cur realm, t *testing.T) {
395	handler := func(title string, action PrivilegedAction) error {
396		return nil
397	}
398
399	// Empty path panics (consistent with NewRestrictedContractAuthority).
400	uassert.PanicsWithMessage(t, cur, "contract path cannot be empty", func() {
401		NewContractAuthority("", handler)
402	})
403
404	// A nil handler panics at CONSTRUCTION rather than being tolerated and
405	// surfaced at Authorize time. Tolerating it produced a permanent brick:
406	// see TestNilHandlerWouldBeUnrotatable below.
407	uassert.PanicsWithMessage(t, cur, "contract handler cannot be nil", func() {
408		NewContractAuthority("gno.land/r/test", nil)
409	})
410
411	// The Authorize-time guard stays for a zero-value ContractAuthority,
412	// which the constructors cannot produce but a struct literal can.
413	code := testing.NewCodeRealm("gno.land/r/test").Address()
414	zero := &ContractAuthority{contractPath: "gno.land/r/test", contractAddr: code}
415	err := zero.Authorize(code, "test", func() error {
416		return nil
417	})
418	uassert.True(t, err != nil, "nil handler authority should fail to authorize")
419
420	// Test valid configuration
421	contractAuth := NewContractAuthority("gno.land/r/test", handler)
422	err = contractAuth.Authorize(code, "test", func() error {
423		return nil
424	})
425	uassert.True(t, err == nil, "valid contract authority should authorize successfully")
426}
427
428func TestAuthorizerString(cur realm, t *testing.T) {
429	auth := NewWithMembers(cur.Address())
430	addr := cur.Address()
431
432	// Test initial string representation
433	str := auth.String()
434	uassert.Equal(t, str, "member_authority["+string(addr)+"]")
435
436	// Test string after transfer — caller is the current member (cur).
437	autoAuth := NewAutoAcceptAuthority()
438	var err error
439	func(cur realm) { err = auth.Transfer(0, cur, autoAuth) }(cross(cur))
440	uassert.True(t, err == nil, "transfer should succeed")
441	str = auth.String()
442	uassert.Equal(t, str, "auto_accept_authority")
443
444	// Test custom authority — auto-accept lets anyone transfer.
445	customAuth := &mockAuthority{}
446	func(cur realm) { err = auth.Transfer(0, cur, customAuth) }(cross(cur))
447	uassert.True(t, err == nil, "transfer should succeed")
448	str = auth.String()
449	uassert.Equal(t, str, "custom_authority[mock]")
450}
451
452type mockAuthority struct{}
453
454func (c mockAuthority) String() string { return "mock" }
455func (a mockAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
456	// autoaccept
457	return action()
458}
459
460func TestAuthorityString(cur realm, t *testing.T) {
461	alice := testutils.TestAddress("alice")
462
463	// MemberAuthority
464	memberAuth := NewMemberAuthority(alice)
465	memberStr := memberAuth.String()
466	expectedMemberStr := "member_authority[g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh]"
467	uassert.Equal(t, memberStr, expectedMemberStr)
468
469	// ContractAuthority — the proposer is rendered, not just the path.
470	// Without it the two constructors below are indistinguishable, which
471	// is what made consumer-level assertions on this string blind to an
472	// authority being swapped for a wide-open one.
473	contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { return nil })
474	contractStr := contractAuth.String()
475	expectedContractStr := "contract_authority[contract=gno.land/r/test,proposer=contract-identity]"
476	uassert.Equal(t, contractStr, expectedContractStr)
477
478	// Same path, same handler, explicit open proposer — must render
479	// differently from the gated default above.
480	openAuth := NewRestrictedContractAuthority(
481		"gno.land/r/test",
482		func(title string, action PrivilegedAction) error { return nil },
483		NewAutoAcceptAuthority(),
484	)
485	uassert.Equal(t,
486		"contract_authority[contract=gno.land/r/test,proposer=auto_accept_authority]",
487		openAuth.String())
488	uassert.NotEqual(t, contractStr, openAuth.String())
489
490	// AutoAcceptAuthority
491	autoAuth := NewAutoAcceptAuthority()
492	autoStr := autoAuth.String()
493	expectedAutoStr := "auto_accept_authority"
494	uassert.Equal(t, autoStr, expectedAutoStr)
495
496	// DroppedAuthority
497	droppedAuth := NewDroppedAuthority()
498	droppedStr := droppedAuth.String()
499	expectedDroppedStr := "dropped_authority"
500	uassert.Equal(t, droppedStr, expectedDroppedStr)
501}
502
503// TestContractAuthorityUnauthorizedCaller verifies the
504// fix: a ContractAuthority's default proposer is the contract itself, so
505// any other caller is rejected UPSTREAM (at the proposer) and the handler
506// and privileged action never run. Previously the default proposer was
507// AutoAcceptAuthority, and the only guard was a handler-side
508// `unsafe.CurrentRealm() == contractAddr` check — which was bypassable
509// and which many consumers (e.g. r/gnops/valopers) omitted entirely,
510// leaving no caller check at all.
511func TestContractAuthorityUnauthorizedCaller(cur realm, t *testing.T) {
512	contractPath := "gno.land/r/testcontract"
513	contractAddr := chain.PackageAddress(contractPath)
514	unauthorizedAddr := testutils.TestAddress("unauthorized")
515
516	// A permissive handler that simply runs the action — the realistic
517	// shape a consumer registers. The package, not the handler, must be
518	// the one that rejects an unauthorized caller.
519	handlerExecuted := false
520	contractHandler := func(title string, action PrivilegedAction) error {
521		handlerExecuted = true
522		return action()
523	}
524	contractAuth := NewContractAuthority(contractPath, contractHandler)
525
526	actionExecuted := false
527	privilegedAction := func() error {
528		actionExecuted = true
529		return nil
530	}
531
532	// 1. Unauthorized caller: rejected by the contract-identity proposer
533	// before the handler or the action can run.
534	err := contractAuth.Authorize(unauthorizedAddr, "test_action", privilegedAction)
535	uassert.Error(t, err, "unauthorized caller must be rejected")
536	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
537	uassert.False(t, handlerExecuted, "handler must not run for an unauthorized caller")
538	uassert.False(t, actionExecuted, "privileged action must not run for an unauthorized caller")
539
540	// 2. The contract itself is authorized.
541	err = contractAuth.Authorize(contractAddr, "test_action", privilegedAction)
542	uassert.NoError(t, err, "the contract itself must be authorized")
543	uassert.True(t, handlerExecuted, "handler must run when the contract is the caller")
544	uassert.True(t, actionExecuted, "privileged action must run when the contract is the caller")
545}
546
547// TestAuthorizerDoByPrevious verifies the "calling realm authorizes"
548// pattern: a function (the inner crossing closure) invokes
549// DoByPrevious so the authority check sees cur.Previous() — the realm
550// that crossed into it — not the function's own realm.
551//
552// Each scenario crosses into the inner closure via cross(cur) after
553// SetRealm — inside the closure, cur is the fresh live cur and
554// cur.Previous() is the SetRealm'd outer realm. This is the only way
555// to exercise DoByPrevious correctly under the IsCurrent guard, which
556// rejects synthetic realm values (testing.MakeRealm) and stored
557// stale captures.
558func TestAuthorizerDoByPrevious(cur realm, t *testing.T) {
559	alice := testutils.TestAddress("alice")
560	bob := testutils.TestAddress("bob")
561
562	auth := NewWithMembers(alice)
563
564	// alice (member) crosses in: cur.Previous() == alice inside the inner closure.
565	testing.SetRealm(testing.NewUserRealm(alice))
566	executed := false
567	args := []any{"test_arg", 123}
568	func(cur realm) {
569		err := auth.DoByPrevious(0, cur, "test_action", func() error {
570			executed = true
571			return nil
572		}, args...)
573		uassert.NoError(t, err, "expected no error")
574		uassert.True(t, executed, "action should have been executed")
575	}(cross(cur))
576
577	expectedErr := errors.New("test error")
578	func(cur realm) {
579		err := auth.DoByPrevious(0, cur, "test_action", func() error {
580			return expectedErr
581		})
582		uassert.ErrorContains(t, err, expectedErr.Error(), "expected error")
583	}(cross(cur))
584
585	// bob (not a member) crosses in: Authorize must reject.
586	testing.SetRealm(testing.NewUserRealm(bob))
587	executed = false
588	func(cur realm) {
589		err := auth.DoByPrevious(0, cur, "test_action", func() error {
590			executed = true
591			return nil
592		}, "unauthorized_arg")
593		uassert.ErrorContains(t, err, "unauthorized", "expected error")
594		uassert.False(t, executed, "action should not have been executed")
595	}(cross(cur))
596}
597
598// ---------------------------------------------------------------------------
599// Contract-identity gate: regression suite.
600//
601// A plain NewContractAuthority now defaults its proposer to the contract
602// ITSELF (MemberAuthority of the contract's own package address) instead of
603// AutoAcceptAuthority. These tests pin the security contract: only the
604// contract may drive privileged actions, an external caller can neither
605// drive nor Transfer the authority, the contract can still rotate its own
606// authority, and the escape hatch for a genuinely-open proposer still
607// exists.
608// ---------------------------------------------------------------------------
609
610// The default proposer accepts the contract itself and rejects everyone
611// else, before the handler or action can run.
612func TestDefaultProposerIsContractOnly(cur realm, t *testing.T) {
613	const path = "gno.land/r/defcontract"
614	contractAddr := chain.PackageAddress(path)
615
616	ran := 0
617	ca := NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
618		return action()
619	})
620
621	// The contract itself: authorized.
622	err := ca.Authorize(contractAddr, "action", func() error { ran++; return nil })
623	uassert.NoError(t, err, "the contract itself must be authorized")
624
625	// Anyone else: rejected at the proposer, action never runs.
626	outsider := testutils.TestAddress("outsider")
627	err = ca.Authorize(outsider, "action", func() error { ran++; return nil })
628	uassert.Error(t, err, "a non-contract caller must be rejected")
629	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
630
631	uassert.Equal(t, 1, ran, "only the contract's action must have run")
632}
633
634// An external realm that holds another realm's Authorizer cannot Transfer
635// it. The caller derived under IsCurrent is that external realm, not the
636// contract, so the rotation is rejected and the authority is left intact.
637func TestExternalTransferRejected(cur realm, t *testing.T) {
638	authorizer := NewWithAuthority(
639		NewContractAuthority("gno.land/r/victim", func(_ string, action PrivilegedAction) error {
640			return action()
641		}),
642	)
643
644	attacker := testutils.TestAddress("attacker")
645	testing.SetRealm(testing.NewUserRealm(attacker))
646
647	var err error
648	func(cur realm) { err = authorizer.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))
649
650	uassert.Error(t, err, "external Transfer must be rejected")
651	uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")
652
653	_, ok := authorizer.Authority().(*ContractAuthority)
654	uassert.True(t, ok, "authority must be unchanged after a rejected rotation")
655}
656
657// Same shape via DoByPrevious (a privileged action instead of a
658// Transfer): an external caller is rejected and the action never runs.
659func TestExternalDoByPreviousRejected(cur realm, t *testing.T) {
660	authorizer := NewWithAuthority(
661		NewContractAuthority("gno.land/r/victim2", func(_ string, action PrivilegedAction) error {
662			return action()
663		}),
664	)
665
666	ran := false
667	testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("randomuser")))
668	var err error
669	func(cur realm) {
670		err = authorizer.DoByPrevious(0, cur, "privileged", func() error { ran = true; return nil })
671	}(cross(cur))
672
673	uassert.Error(t, err, "external DoByPrevious must be rejected")
674	uassert.False(t, ran, "privileged action must not run for an external caller")
675}
676
677// The contract-identity model does NOT lock out legitimate governance: the
678// contract can still rotate (Transfer) its own authority when the transfer
679// is driven with the contract as the previous realm (caller == contractAddr).
680func TestContractCanRotateItsOwnAuthority(cur realm, t *testing.T) {
681	const path = "gno.land/r/selfgov"
682	authorizer := NewWithAuthority(
683		NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
684			return action()
685		}),
686	)
687
688	// Drive the transfer with the contract as the caller: set the outer
689	// realm to the contract, then cross into a closure so cur.Previous()
690	// resolves to the contract.
691	testing.SetRealm(testing.NewCodeRealm(path))
692	var err error
693	func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
694
695	uassert.NoError(t, err, "the contract itself must be able to rotate its authority")
696	_, ok := authorizer.Authority().(*AutoAcceptAuthority)
697	uassert.True(t, ok, "authority should have rotated to auto-accept")
698}
699
700// Boundary test: the raw Authority.Authorize takes `caller` as a plain
701// parameter, so a holder of the raw Authority can forge caller ==
702// contractAddr. This proves that even so, a forged-caller raw Authorize
703// only runs the action the caller itself supplies — it CANNOT mutate an
704// Authorizer's installed authority, because the Transfer mutation lives
705// only inside Authorizer.Transfer (which derives caller non-forgeably).
706// The lesson encoded here: consumers must reach a ContractAuthority through
707// the Authorizer wrapper and must not hand out the raw Authority.
708//
709// WHAT THIS TEST DOES AND DOES NOT FREEZE. Only the final assertion is a
710// security property. That raw Authorize ACCEPTS a forged caller is
711// current behaviour, not a guarantee — it is the consequence of `caller`
712// being a parameter on an exported interface method. If someone later
713// hardens that path so raw Authorize stops accepting a forged caller,
714// this test going red means the test is stale, NOT that the change is
715// wrong: delete the two intermediate assertions and keep the last one.
716// (Recorded because asserting NoError here reads like the forgeability
717// is wanted. It is tolerated, and only because of what follows.)
718func TestForgedCallerCannotTransfer(cur realm, t *testing.T) {
719	const path = "gno.land/r/boundary"
720	contractAddr := chain.PackageAddress(path)
721
722	authorizer := NewWithAuthority(
723		NewContractAuthority(path, func(_ string, action PrivilegedAction) error {
724			return action()
725		}),
726	)
727
728	// Attacker obtains the raw Authority and forges the contract as caller.
729	attackerRan := false
730	err := authorizer.Authority().Authorize(contractAddr, "transfer_authority", func() error {
731		attackerRan = true
732		return nil
733	})
734
735	// Current behaviour, documented above — not a property to preserve.
736	uassert.NoError(t, err, "raw Authorize currently accepts a forged caller (see comment)")
737	uassert.True(t, attackerRan, "only the caller's own inert closure runs")
738
739	// THE property: the installed authority is UNCHANGED. Raw Authorize
740	// cannot perform a Transfer — the mutation closure is private to
741	// Authorizer.Transfer, which derives caller non-forgeably.
742	_, ok := authorizer.Authority().(*ContractAuthority)
743	uassert.True(t, ok, "raw Authorize must not be able to transfer the authority")
744}
745
746// Two contract authorities on different paths never cross-authorize.
747func TestContractPathIsolation(cur realm, t *testing.T) {
748	caB := NewContractAuthority("gno.land/r/pathb", func(_ string, action PrivilegedAction) error {
749		return action()
750	})
751	addrA := chain.PackageAddress("gno.land/r/patha")
752
753	ran := false
754	err := caB.Authorize(addrA, "action", func() error { ran = true; return nil })
755	uassert.Error(t, err, "contract A must not authorize contract B's authority")
756	uassert.False(t, ran, "action must not run across contract identities")
757}
758
759// Escape hatch: a consumer that genuinely wants open proposals can still
760// opt in via NewRestrictedContractAuthority with an AutoAcceptAuthority
761// proposer — restoring the pre-fix "anyone can propose" behavior explicitly.
762func TestRestrictedAutoAcceptEscapeHatch(cur realm, t *testing.T) {
763	ca := NewRestrictedContractAuthority(
764		"gno.land/r/open",
765		func(_ string, action PrivilegedAction) error { return action() },
766		NewAutoAcceptAuthority(),
767	)
768
769	ran := false
770	err := ca.Authorize(testutils.TestAddress("anyone"), "action", func() error { ran = true; return nil })
771	uassert.NoError(t, err, "an explicit AutoAccept proposer restores open proposals")
772	uassert.True(t, ran, "action must run under an explicit AutoAccept proposer")
773}
774
775// Why the nil handler is now rejected at construction rather than
776// tolerated: Authorize checks contractHandler == nil BEFORE consulting the
777// proposer, and Transfer routes through Authorize, so there is no rotation
778// path out. It is a permanent brick reachable by an ordinary deployer
779// typo -- the same bricked-governance failure mode, by accident.
780//
781// The zero value is used because the constructor no longer allows it.
782func TestNilHandlerWouldBeUnrotatable(cur realm, t *testing.T) {
783	const path = "gno.land/r/nilbrick"
784	addr := chain.PackageAddress(path)
785	authorizer := NewWithAuthority(&ContractAuthority{
786		contractPath: path,
787		contractAddr: addr,
788		// Both handler and proposer are nil: a struct literal is the only
789		// way to reach this, and Authorize fails closed on either.
790	})
791
792	// Drive the rotation as the contract itself -- the only principal the
793	// default proposer accepts. If this cannot escape, nobody can.
794	testing.SetRealm(testing.NewCodeRealm(path))
795	var err error
796	func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))
797
798	uassert.Error(t, err, "a nil-handler authority must not be silently rotatable")
799	_, stuck := authorizer.Authority().(*ContractAuthority)
800	uassert.True(t, stuck, "nil-handler authority is unrotatable -- hence rejected at construction")
801}
802
803// A crossing closure declared HERE carries this package's identity, not
804// the contract path's -- testing.SetRealm does not change that, because
805// the VM mints a crossing frame's realm from the callee's declaring
806// package. So the DoByCurrent shape that Example_contractAuthority teaches
807// cannot be exercised from inside this package at all; it needs a real
808// realm. It is covered by filetests/z_contract_authority_shape_filetest.gno.
809//
810// The same property is why an exported crossing closure leaks a realm's
811// authority to any caller -- it is one mechanism seen from two
812// sides. This test pins the half that is observable here: an unrelated
813// package's frame is rejected.
814func TestForeignFrameCannotDriveContractAuthority(cur realm, t *testing.T) {
815	auth := NewWithAuthority(NewContractAuthority("gno.land/r/demo/dao", mockDAOHandler))
816
817	testing.SetRealm(testing.NewCodeRealm("gno.land/r/example"))
818	ran := false
819	var err error
820	func(cur realm) {
821		err = auth.DoByCurrent(0, cur, "update_params", func() error {
822			ran = true
823			return nil
824		})
825	}(cross(cur))
826
827	uassert.Error(t, err, "a frame from another package must not drive this authority")
828	uassert.False(t, ran, "the action must not run")
829}
830
831// A malformed contract path binds the authority to an address no realm can
832// ever present. Because Transfer routes through the same gate, such an
833// authority is also unrotatable: a permanent brick, and the same failure
834// mode the nil-handler panic exists to prevent. Rejected at construction.
835//
836// Pre-fix these all constructed happily; the AutoAccept default made the
837// typo harmless (and insecure), so nothing complained.
838func TestContractPathValidationRejectsMalformed(cur realm, t *testing.T) {
839	handler := func(_ string, action PrivilegedAction) error { return action() }
840
841	for _, path := range []string{
842		"gno.land/r/gov/dao ",  // trailing space
843		" gno.land/r/gov/dao",  // leading space
844		"gno.land/r/gov/dao\n", // trailing newline
845		"gno.land/r/gov dao",   // embedded space
846		" ",
847		"not a path",
848		"GNO.LAND/R/GOV/DAO", // uppercase: not a legal gno pkgpath
849		"gno.land//r/gov/dao",
850		"gno.land/r/gov/dao/",
851		"singlesegment",
852	} {
853		uassert.PanicsContains(t, cur, "contract path", func() {
854			NewContractAuthority(path, handler)
855		}, "malformed path must be rejected: "+path)
856		uassert.PanicsContains(t, cur, "contract path", func() {
857			NewRestrictedContractAuthority(path, handler, NewAutoAcceptAuthority())
858		}, "malformed path must be rejected by the restricted ctor too: "+path)
859	}
860
861	// Real paths still construct, including dashes, digits, underscores and
862	// version suffixes.
863	for _, path := range []string{
864		"gno.land/r/gov/dao",
865		"gno.land/r/gnops/valopers",
866		"gno.land/p/moul/authz/v0",
867		"gno.land/r/sys/validators/v0",
868		"gno.land/r/some-user/my_pkg2",
869	} {
870		uassert.NotPanics(t, cur, func() {
871			NewContractAuthority(path, handler)
872		}, "legitimate path must construct: "+path)
873	}
874}
875
876// spoofProposer is a wide-open Authority that LIES in String(), returning
877// the same text the canonical contract-identity default renders.
878type spoofProposer struct{}
879
880func (spoofProposer) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
881	return action()
882}
883
884func (spoofProposer) String() string { return "contract-identity" }
885
886// A foreign proposer must not be able to impersonate the gated default in
887// the rendered description. ContractAuthority.String() is promoted by this
888// package as the one surface distinguishing a gated authority from a
889// wide-open one, and consumers assert on it (r/gnops/valopers.Auth()). An
890// impl that chooses its own String() text defeated that: a fully permissive
891// authority rendered byte-identical to the default, so every string-based
892// configuration pin stayed green while the gate was gone.
893func TestSpoofedProposerCannotImpersonateContractIdentity(cur realm, t *testing.T) {
894	const path = "gno.land/r/spoofprobe"
895	handler := func(_ string, action PrivilegedAction) error { return action() }
896	outsider := chain.PackageAddress("gno.land/r/outsider")
897
898	gated := NewContractAuthority(path, handler)
899	spoof := NewRestrictedContractAuthority(path, handler, spoofProposer{})
900
901	// The behavioural difference the strings must reflect.
902	uassert.Error(t, gated.Authorize(outsider, "t", func() error { return nil }),
903		"the gated default must reject an outsider")
904	uassert.NoError(t, spoof.Authorize(outsider, "t", func() error { return nil }),
905		"the spoofing proposer is wide open -- that is the point of the test")
906
907	// ...and they do.
908	uassert.NotEqual(t, gated.String(), spoof.String(),
909		"a wide-open authority must not render identically to the gated default")
910	uassert.Equal(t,
911		"contract_authority[contract="+path+",proposer=contract-identity]",
912		gated.String())
913	uassert.Equal(t,
914		"contract_authority[contract="+path+",proposer=custom_authority[contract-identity]]",
915		spoof.String(),
916		"a non-canonical proposer must be wrapped, not trusted to name itself")
917
918	// Same guarantee through the Authorizer wrapper, which is the shape a
919	// consumer realm actually exposes.
920	uassert.NotEqual(t,
921		NewWithAuthority(gated).String(),
922		NewWithAuthority(spoof).String())
923}
924
925// An explicit proposer REPLACES the contract-identity gate; it does not add
926// to it. NewRestrictedContractAuthority(govdaoPath, h, member(alice)) means
927// "alice, and not GovDAO" -- contractAddr is never consulted. Pinned because
928// the rendered string reads like a conjunction and the old godoc said
929// "widen", so a consumer could reasonably have expected "both".
930func TestExplicitProposerReplacesIdentityGate(cur realm, t *testing.T) {
931	const path = "gno.land/r/gov/dao"
932	handler := func(_ string, action PrivilegedAction) error { return action() }
933	govdao := chain.PackageAddress(path)
934	alice := chain.PackageAddress("gno.land/r/alice")
935
936	gated := NewContractAuthority(path, handler)
937	replaced := NewRestrictedContractAuthority(path, handler, NewMemberAuthority(alice))
938
939	uassert.NoError(t, gated.Authorize(govdao, "x", func() error { return nil }),
940		"the default gate accepts the bound contract")
941	uassert.Error(t, gated.Authorize(alice, "x", func() error { return nil }),
942		"the default gate rejects everyone else")
943
944	uassert.Error(t, replaced.Authorize(govdao, "x", func() error { return nil }),
945		"an explicit proposer REPLACES the identity gate: govdao is no longer accepted")
946	uassert.NoError(t, replaced.Authorize(alice, "x", func() error { return nil }),
947		"only the explicit proposer's principal is accepted")
948}