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