1package authz23import (4 "chain"5 "errors"6 "strings"7 "testing"89 "gno.land/p/nt/testutils/v0"10 "gno.land/p/nt/uassert/v0"11)1213func TestNewWithCurrent(cur realm, t *testing.T) {14 alice := testutils.TestAddress("alice")15 testing.SetRealm(testing.NewUserRealm(alice))1617 auth := NewWithMembers(cur.Address())1819 // Check that the current authority is a MemberAuthority20 memberAuth, ok := auth.Authority().(*MemberAuthority)21 uassert.True(t, ok, "expected MemberAuthority")2223 // Check that the caller is a member24 uassert.True(t, memberAuth.Has(alice), "caller should be a member")2526 // Check string representation27 uassert.True(t, strings.Contains(auth.String(), alice.String()))28}2930func TestNewWithAuthority(cur realm, t *testing.T) {31 alice := testutils.TestAddress("alice")32 memberAuth := NewMemberAuthority(alice)3334 auth := NewWithAuthority(memberAuth)3536 // Check that the current authority is the one we provided37 uassert.True(t, auth.Authority() == memberAuth, "expected provided authority")38}3940func TestAuthorizerAuthorize(cur realm, t *testing.T) {41 alice := testutils.TestAddress("alice")42 testing.SetRealm(testing.NewUserRealm(alice))4344 auth := NewWithMembers(cur.Address())4546 // Test successful action with args47 executed := false48 args := []any{"test_arg", 123}49 err := auth.DoByCurrent(0, cur, "test_action", func() error {50 executed = true51 return nil52 }, args...)5354 uassert.True(t, err == nil, "expected no error")55 uassert.True(t, executed, "action should have been executed")5657 // Test unauthorized action with args58 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("bob")))5960 executed = false61 err = auth.DoByCurrent(0, cur, "test_action", func() error {62 executed = true63 return nil64 }, "unauthorized_arg")6566 uassert.True(t, err != nil, "expected error")67 uassert.False(t, executed, "action should not have been executed")6869 // Test action returning error70 testing.SetRealm(testing.NewUserRealm(alice))71 expectedErr := errors.New("test error")7273 err = auth.DoByCurrent(0, cur, "test_action", func() error {74 return expectedErr75 })7677 uassert.True(t, err == expectedErr, "expected specific error")78}7980func TestAuthorizerTransfer(cur realm, t *testing.T) {81 alice := testutils.TestAddress("alice")82 testing.SetRealm(testing.NewUserRealm(alice))8384 auth := NewWithMembers(cur.Address())8586 // Test transfer to new member authority87 bob := testutils.TestAddress("bob")88 newAuth := NewMemberAuthority(bob)8990 var err error91 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")9495 // Test unauthorized transfer: principal is not a member of newAuth.96 carol := testutils.TestAddress("carol")97 testing.SetRealm(testing.NewUserRealm(carol))9899 func(cur realm) { err = auth.Transfer(0, cur, NewMemberAuthority(alice)) }(cross(cur))100 uassert.True(t, err != nil, "expected error")101102 // 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 })107108 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}112113func TestAuthorizerTransferChain(cur realm, t *testing.T) {114 alice := testutils.TestAddress("alice")115 testing.SetRealm(testing.NewUserRealm(alice))116117 // Create a chain of transfers118 auth := NewWithMembers(cur.Address())119120 // First transfer to a new member authority121 bob := testutils.TestAddress("bob")122 memberAuth := NewMemberAuthority(bob)123124 var err error125 func(cur realm) { err = auth.Transfer(0, cur, memberAuth) }(cross(cur))126 uassert.True(t, err == nil, "unexpected error in first transfer")127128 // 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")135136 // Finally transfer to an auto-accept authority. The authority is now137 // the contract authority, whose default proposer is the contract138 // itself, so the transfer's caller —139 // cur.Previous() — must be the contract. Set the outer realm to the140 // contract, then cross into a closure so cur.Previous() resolves to141 // 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}148149func TestAuthorizerTransferUnauthorizedRejected(cur realm, t *testing.T) {150 // Regression for the address-parameter forgery: previously Transfer151 // took a caller-supplied `caller address` that an attacker realm could152 // set to the real owner. After the IsCurrent + rlm.Previous() fix, the153 // principal is the captured cur's previous and cannot be forged.154 admin := testutils.TestAddress("admin")155 attacker := testutils.TestAddress("attacker")156157 testing.SetRealm(testing.NewUserRealm(admin))158 auth := NewWithMembers(cur.Address()) // admin is the initial authority159160 initialAuth, ok := auth.Authority().(*MemberAuthority)161 uassert.True(t, ok)162 uassert.True(t, initialAuth.Has(admin))163 uassert.False(t, initialAuth.Has(attacker))164165 // Attacker context: cur.Previous() inside the closure will be attacker.166 testing.SetRealm(testing.NewUserRealm(attacker))167 attackerAuth := NewMemberAuthority(attacker)168 var err error169 func(cur realm) { err = auth.Transfer(0, cur, attackerAuth) }(cross(cur))170171 uassert.True(t, err != nil, "attacker transfer must be rejected")172173 // 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}180181func TestAuthorizerWithDroppedAuthority(cur realm, t *testing.T) {182 alice := testutils.TestAddress("alice")183 testing.SetRealm(testing.NewUserRealm(alice))184185 auth := NewWithMembers(cur.Address())186187 // Transfer to dropped authority188 var err error189 func(cur realm) { err = auth.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))190 uassert.True(t, err == nil, "expected no error")191192 // Try to execute action193 err = auth.DoByCurrent(0, cur, "test_action", func() error {194 return nil195 })196 uassert.True(t, err != nil, "expected error from dropped authority")197198 // Try to transfer again199 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}202203func TestContractAuthorityHandlerExecutionOnce(cur realm, t *testing.T) {204 attempts := 0205 executed := 0206207 contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {208 // Try to execute the action twice in the same handler209 if err := action(); err != nil {210 return err211 }212 attempts++213214 // Second execution should fail215 if err := action(); err != nil {216 return err217 }218 attempts++219 return nil220 })221222 // Set caller to contract address223 codeRealm := testing.NewCodeRealm("gno.land/r/test")224 testing.SetRealm(codeRealm)225 code := codeRealm.Address()226227 testArgs := []any{"proposal_id", 42, "metadata", map[string]string{"key": "value"}}228 err := contractAuth.Authorize(code, "test_action", func() error {229 executed++230 return nil231 }, testArgs...)232233 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}237238func TestContractAuthorityExecutionTwice(cur realm, t *testing.T) {239 executed := 0240241 contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {242 return action()243 })244245 // Set caller to contract address246 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"}}250251 err := contractAuth.Authorize(code, "test_action", func() error {252 executed++253 return nil254 }, testArgs...)255256 uassert.True(t, err == nil, "handler execution should succeed")257 uassert.True(t, executed == 1, "handler should have executed once")258259 // A new action, even with the same title, should be executed260 err = contractAuth.Authorize(code, "test_action", func() error {261 executed++262 return nil263 }, testArgs...)264265 uassert.True(t, err == nil, "handler execution should succeed")266 uassert.True(t, executed == 2, "handler should have executed twice")267}268269func TestContractAuthorityWithProposer(cur realm, t *testing.T) {270 alice := testutils.TestAddress("alice")271 memberAuth := NewMemberAuthority(alice)272273 handlerCalled := false274 actionExecuted := false275276 contractAuth := NewRestrictedContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error {277 handlerCalled = true278 // Set caller to contract address before executing action279 testing.SetRealm(testing.NewCodeRealm("gno.land/r/test"))280 return action()281 }, memberAuth)282283 // Test authorized member284 testArgs := []any{"proposal_metadata", "test value"}285 err := contractAuth.Authorize(alice, "test_action", func() error {286 actionExecuted = true287 return nil288 }, testArgs...)289290 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")293294 // Reset flags for unauthorized test295 handlerCalled = false296 actionExecuted = false297298 // Test unauthorized proposer299 bob := testutils.TestAddress("bob")300 err = contractAuth.Authorize(bob, "test_action", func() error {301 actionExecuted = true302 return nil303 }, testArgs...)304305 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}309310func TestAutoAcceptAuthority(cur realm, t *testing.T) {311 alice := testutils.TestAddress("alice")312 auth := NewAutoAcceptAuthority()313314 // Test that any action is authorized315 executed := false316 err := auth.Authorize(alice, "test_action", func() error {317 executed = true318 return nil319 })320321 uassert.True(t, err == nil, "auto-accept should not return error")322 uassert.True(t, executed, "action should have been executed")323324 // Test with different caller325 random := testutils.TestAddress("random")326 executed = false327 err = auth.Authorize(random, "test_action", func() error {328 executed = true329 return nil330 })331332 uassert.True(t, err == nil, "auto-accept should not care about caller")333 uassert.True(t, executed, "action should have been executed")334}335336func TestAutoAcceptAuthorityWithArgs(cur realm, t *testing.T) {337 auth := NewAutoAcceptAuthority()338 anyuser := testutils.TestAddress("anyuser")339340 // Test that any action is authorized with args341 executed := false342 testArgs := []any{"arg1", 42, "arg3"}343 err := auth.Authorize(anyuser, "test_action", func() error {344 executed = true345 return nil346 }, testArgs...)347348 uassert.True(t, err == nil, "auto-accept should not return error")349 uassert.True(t, executed, "action should have been executed")350}351352func TestMemberAuthorityMultipleMembers(cur realm, t *testing.T) {353 alice := testutils.TestAddress("alice")354 bob := testutils.TestAddress("bob")355 carol := testutils.TestAddress("carol")356357 // Create authority with multiple members358 auth := NewMemberAuthority(alice, bob)359360 // Test that both members can execute actions361 for _, member := range []address{alice, bob} {362 err := auth.Authorize(member, "test_action", func() error {363 return nil364 })365 uassert.True(t, err == nil, "member should be authorized")366 }367368 // Test that non-member cannot execute369 err := auth.Authorize(carol, "test_action", func() error {370 return nil371 })372 uassert.True(t, err != nil, "non-member should not be authorized")373374 // Test Tree() functionality375 tree := auth.Tree()376 uassert.True(t, tree.Size() == 2, "tree should have 2 members")377378 // Verify both members are in the tree379 found := make(map[address]bool)380 tree.Iterate("", "", func(key string, _ any) bool {381 found[address(key)] = true382 return false383 })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")387388 // Test read-only nature of the tree389 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 panic394}395396func TestAuthorizerCurrentNeverNil(cur realm, t *testing.T) {397 auth := NewWithMembers(cur.Address())398399 // Authority should never be nil after initialization400 uassert.True(t, auth.Authority() != nil, "current authority should not be nil")401402 // Authority should not be nil after transfer403 var err error404 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}408409func TestContractAuthorityValidation(cur realm, t *testing.T) {410 handler := func(title string, action PrivilegedAction) error {411 return nil412 }413414 // Empty path panics (consistent with NewRestrictedContractAuthority).415 uassert.PanicsWithMessage(t, cur, "contract path cannot be empty", func() {416 NewContractAuthority("", handler)417 })418419 // A nil handler panics at CONSTRUCTION rather than being tolerated and420 // 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 })425426 // 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 nil432 })433 uassert.True(t, err != nil, "nil handler authority should fail to authorize")434435 // Test valid configuration436 contractAuth := NewContractAuthority("gno.land/r/test", handler)437 err = contractAuth.Authorize(code, "test", func() error {438 return nil439 })440 uassert.True(t, err == nil, "valid contract authority should authorize successfully")441}442443func TestAuthorizerString(cur realm, t *testing.T) {444 auth := NewWithMembers(cur.Address())445 addr := cur.Address()446447 // Test initial string representation448 str := auth.String()449 uassert.Equal(t, str, "member_authority["+string(addr)+"]")450451 // Test string after transfer — caller is the current member (cur).452 autoAuth := NewAutoAcceptAuthority()453 var err error454 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")458459 // 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}466467type mockAuthority struct{}468469func (c mockAuthority) String() string { return "mock" }470func (a mockAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {471 // autoaccept472 return action()473}474475func TestAuthorityString(cur realm, t *testing.T) {476 alice := testutils.TestAddress("alice")477478 // MemberAuthority479 memberAuth := NewMemberAuthority(alice)480 memberStr := memberAuth.String()481 expectedMemberStr := "member_authority[g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh]"482 uassert.Equal(t, memberStr, expectedMemberStr)483484 // ContractAuthority — the proposer is rendered, not just the path.485 // Without it the two constructors below are indistinguishable, which486 // is what made consumer-level assertions on this string blind to an487 // 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)492493 // Same path, same handler, explicit open proposer — must render494 // 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())504505 // AutoAcceptAuthority506 autoAuth := NewAutoAcceptAuthority()507 autoStr := autoAuth.String()508 expectedAutoStr := "auto_accept_authority"509 uassert.Equal(t, autoStr, expectedAutoStr)510511 // DroppedAuthority512 droppedAuth := NewDroppedAuthority()513 droppedStr := droppedAuth.String()514 expectedDroppedStr := "dropped_authority"515 uassert.Equal(t, droppedStr, expectedDroppedStr)516}517518// TestContractAuthorityUnauthorizedCaller verifies the519// fix: a ContractAuthority's default proposer is the contract itself, so520// any other caller is rejected UPSTREAM (at the proposer) and the handler521// and privileged action never run. Previously the default proposer was522// AutoAcceptAuthority, and the only guard was a handler-side523// `unsafe.CurrentRealm() == contractAddr` check — which was bypassable524// 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")530531 // A permissive handler that simply runs the action — the realistic532 // shape a consumer registers. The package, not the handler, must be533 // the one that rejects an unauthorized caller.534 handlerExecuted := false535 contractHandler := func(title string, action PrivilegedAction) error {536 handlerExecuted = true537 return action()538 }539 contractAuth := NewContractAuthority(contractPath, contractHandler)540541 actionExecuted := false542 privilegedAction := func() error {543 actionExecuted = true544 return nil545 }546547 // 1. Unauthorized caller: rejected by the contract-identity proposer548 // 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")554555 // 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}561562// TestAuthorizerDoByPrevious verifies the "calling realm authorizes"563// pattern: a function (the inner crossing closure) invokes564// DoByPrevious so the authority check sees cur.Previous() — the realm565// that crossed into it — not the function's own realm.566//567// Each scenario crosses into the inner closure via cross(cur) after568// SetRealm — inside the closure, cur is the fresh live cur and569// cur.Previous() is the SetRealm'd outer realm. This is the only way570// to exercise DoByPrevious correctly under the IsCurrent guard, which571// rejects synthetic realm values (testing.MakeRealm) and stored572// stale captures.573func TestAuthorizerDoByPrevious(cur realm, t *testing.T) {574 alice := testutils.TestAddress("alice")575 bob := testutils.TestAddress("bob")576577 auth := NewWithMembers(alice)578579 // alice (member) crosses in: cur.Previous() == alice inside the inner closure.580 testing.SetRealm(testing.NewUserRealm(alice))581 executed := false582 args := []any{"test_arg", 123}583 func(cur realm) {584 err := auth.DoByPrevious(0, cur, "test_action", func() error {585 executed = true586 return nil587 }, args...)588 uassert.NoError(t, err, "expected no error")589 uassert.True(t, executed, "action should have been executed")590 }(cross(cur))591592 expectedErr := errors.New("test error")593 func(cur realm) {594 err := auth.DoByPrevious(0, cur, "test_action", func() error {595 return expectedErr596 })597 uassert.ErrorContains(t, err, expectedErr.Error(), "expected error")598 }(cross(cur))599600 // bob (not a member) crosses in: Authorize must reject.601 testing.SetRealm(testing.NewUserRealm(bob))602 executed = false603 func(cur realm) {604 err := auth.DoByPrevious(0, cur, "test_action", func() error {605 executed = true606 return nil607 }, "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}612613// ---------------------------------------------------------------------------614// Contract-identity gate: regression suite.615//616// A plain NewContractAuthority now defaults its proposer to the contract617// ITSELF (MemberAuthority of the contract's own package address) instead of618// AutoAcceptAuthority. These tests pin the security contract: only the619// contract may drive privileged actions, an external caller can neither620// drive nor Transfer the authority, the contract can still rotate its own621// authority, and the escape hatch for a genuinely-open proposer still622// exists.623// ---------------------------------------------------------------------------624625// The default proposer accepts the contract itself and rejects everyone626// 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)630631 ran := 0632 ca := NewContractAuthority(path, func(_ string, action PrivilegedAction) error {633 return action()634 })635636 // 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")639640 // 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")645646 uassert.Equal(t, 1, ran, "only the contract's action must have run")647}648649// An external realm that holds another realm's Authorizer cannot Transfer650// it. The caller derived under IsCurrent is that external realm, not the651// 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 )658659 attacker := testutils.TestAddress("attacker")660 testing.SetRealm(testing.NewUserRealm(attacker))661662 var err error663 func(cur realm) { err = authorizer.Transfer(0, cur, NewDroppedAuthority()) }(cross(cur))664665 uassert.Error(t, err, "external Transfer must be rejected")666 uassert.ErrorContains(t, err, "unauthorized", "rejection must come from the contract-identity proposer")667668 _, ok := authorizer.Authority().(*ContractAuthority)669 uassert.True(t, ok, "authority must be unchanged after a rejected rotation")670}671672// Same shape via DoByPrevious (a privileged action instead of a673// 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 )680681 ran := false682 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("randomuser")))683 var err error684 func(cur realm) {685 err = authorizer.DoByPrevious(0, cur, "privileged", func() error { ran = true; return nil })686 }(cross(cur))687688 uassert.Error(t, err, "external DoByPrevious must be rejected")689 uassert.False(t, ran, "privileged action must not run for an external caller")690}691692// The contract-identity model does NOT lock out legitimate governance: the693// contract can still rotate (Transfer) its own authority when the transfer694// 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 )702703 // Drive the transfer with the contract as the caller: set the outer704 // 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 error708 func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))709710 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}714715// Boundary test: the raw Authority.Authorize takes `caller` as a plain716// parameter, so a holder of the raw Authority can forge caller ==717// contractAddr. This proves that even so, a forged-caller raw Authorize718// only runs the action the caller itself supplies — it CANNOT mutate an719// Authorizer's installed authority, because the Transfer mutation lives720// only inside Authorizer.Transfer (which derives caller non-forgeably).721// The lesson encoded here: consumers must reach a ContractAuthority through722// 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 a725// security property. That raw Authorize ACCEPTS a forged caller is726// current behaviour, not a guarantee — it is the consequence of `caller`727// being a parameter on an exported interface method. If someone later728// 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 is730// wrong: delete the two intermediate assertions and keep the last one.731// (Recorded because asserting NoError here reads like the forgeability732// 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)736737 authorizer := NewWithAuthority(738 NewContractAuthority(path, func(_ string, action PrivilegedAction) error {739 return action()740 }),741 )742743 // Attacker obtains the raw Authority and forges the contract as caller.744 attackerRan := false745 err := authorizer.Authority().Authorize(contractAddr, "transfer_authority", func() error {746 attackerRan = true747 return nil748 })749750 // 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")753754 // THE property: the installed authority is UNCHANGED. Raw Authorize755 // cannot perform a Transfer — the mutation closure is private to756 // 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}760761// 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")767768 ran := false769 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}773774// Escape hatch: a consumer that genuinely wants open proposals can still775// opt in via NewRestrictedContractAuthority with an AutoAcceptAuthority776// 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 )783784 ran := false785 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}789790// Why the nil handler is now rejected at construction rather than791// tolerated: Authorize checks contractHandler == nil BEFORE consulting the792// proposer, and Transfer routes through Authorize, so there is no rotation793// path out. It is a permanent brick reachable by an ordinary deployer794// 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 only804 // way to reach this, and Authorize fails closed on either.805 })806807 // Drive the rotation as the contract itself -- the only principal the808 // default proposer accepts. If this cannot escape, nobody can.809 testing.SetRealm(testing.NewCodeRealm(path))810 var err error811 func(cur realm) { err = authorizer.Transfer(0, cur, NewAutoAcceptAuthority()) }(cross(cur))812813 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}817818// A crossing closure declared HERE carries this package's identity, not819// the contract path's -- testing.SetRealm does not change that, because820// the VM mints a crossing frame's realm from the callee's declaring821// package. So the DoByCurrent shape that Example_contractAuthority teaches822// cannot be exercised from inside this package at all; it needs a real823// 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's826// authority to any caller -- it is one mechanism seen from two827// sides. This test pins the half that is observable here: an unrelated828// package's frame is rejected.829func TestForeignFrameCannotDriveContractAuthority(cur realm, t *testing.T) {830 auth := NewWithAuthority(NewContractAuthority("gno.land/r/demo/dao", mockDAOHandler))831832 testing.SetRealm(testing.NewCodeRealm("gno.land/r/example"))833 ran := false834 var err error835 func(cur realm) {836 err = auth.DoByCurrent(0, cur, "update_params", func() error {837 ran = true838 return nil839 })840 }(cross(cur))841842 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}845846// A malformed contract path binds the authority to an address no realm can847// ever present. Because Transfer routes through the same gate, such an848// authority is also unrotatable: a permanent brick, and the same failure849// mode the nil-handler panic exists to prevent. Rejected at construction.850//851// Pre-fix these all constructed happily; the AutoAccept default made the852// typo harmless (and insecure), so nothing complained.853func TestContractPathValidationRejectsMalformed(cur realm, t *testing.T) {854 handler := func(_ string, action PrivilegedAction) error { return action() }855856 for _, path := range []string{857 "gno.land/r/gov/dao ", // trailing space858 " gno.land/r/gov/dao", // leading space859 "gno.land/r/gov/dao\n", // trailing newline860 "gno.land/r/gov dao", // embedded space861 " ",862 "not a path",863 "GNO.LAND/R/GOV/DAO", // uppercase: not a legal gno pkgpath864 "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 }875876 // Real paths still construct, including dashes, digits, underscores and877 // 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}890891// spoofProposer is a wide-open Authority that LIES in String(), returning892// the same text the canonical contract-identity default renders.893type spoofProposer struct{}894895func (spoofProposer) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {896 return action()897}898899func (spoofProposer) String() string { return "contract-identity" }900901// A foreign proposer must not be able to impersonate the gated default in902// the rendered description. ContractAuthority.String() is promoted by this903// package as the one surface distinguishing a gated authority from a904// wide-open one, and consumers assert on it (r/gnops/valopers.Auth()). An905// impl that chooses its own String() text defeated that: a fully permissive906// authority rendered byte-identical to the default, so every string-based907// 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")912913 gated := NewContractAuthority(path, handler)914 spoof := NewRestrictedContractAuthority(path, handler, spoofProposer{})915916 // 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")921922 // ...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")932933 // Same guarantee through the Authorizer wrapper, which is the shape a934 // consumer realm actually exposes.935 uassert.NotEqual(t,936 NewWithAuthority(gated).String(),937 NewWithAuthority(spoof).String())938}939940// An explicit proposer REPLACES the contract-identity gate; it does not add941// to it. NewRestrictedContractAuthority(govdaoPath, h, member(alice)) means942// "alice, and not GovDAO" -- contractAddr is never consulted. Pinned because943// the rendered string reads like a conjunction and the old godoc said944// "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")950951 gated := NewContractAuthority(path, handler)952 replaced := NewRestrictedContractAuthority(path, handler, NewMemberAuthority(alice))953954 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")958959 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}964Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.