Search Apps Documentation Source Content File Folder Download Copy Actions Download

authz.gno

9.77 Kb ยท 334 lines
  1// Package authz provides flexible authorization control for privileged actions.
  2//
  3// # Authorization Strategies
  4//
  5// The package supports multiple authorization strategies:
  6//   - Member-based: Single user or team of users
  7//   - Contract-based: Async authorization (e.g., via DAO)
  8//   - Auto-accept: Allow all actions
  9//   - Drop: Deny all actions
 10//
 11// Core Components
 12//
 13//   - Authority interface: Base interface implemented by all authorities
 14//   - Authorizer: Main wrapper object for authority management
 15//   - MemberAuthority: Manages authorized addresses
 16//   - ContractAuthority: Delegates to another contract
 17//   - AutoAcceptAuthority: Accepts all actions
 18//   - DroppedAuthority: Denies all actions
 19//
 20// Quick Start
 21//
 22//	// Initialize with contract deployer as authority
 23//	var member address(...)
 24//	var auth = authz.NewWithMembers(member)
 25//
 26//	// Create functions that require authorization
 27//	func UpdateConfig(newValue string) error {
 28//		crossing()
 29//		return auth.DoByPrevious("update_config", func() error {
 30//			config = newValue
 31//			return nil
 32//		})
 33//	}
 34//
 35// See example_test.gno for more usage examples.
 36package authz
 37
 38import (
 39	"chain"
 40	"chain/runtime"
 41	"errors"
 42	"strings"
 43
 44	"gno.land/p/moul/addrset"
 45	"gno.land/p/moul/once"
 46	"gno.land/p/nt/avl"
 47	"gno.land/p/nt/avl/rotree"
 48	"gno.land/p/nt/ufmt"
 49)
 50
 51// Authorizer is the main wrapper object that handles authority management.
 52// It is configured with a replaceable Authority implementation.
 53type Authorizer struct {
 54	auth Authority
 55}
 56
 57// Authority represents an entity that can authorize privileged actions.
 58// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,
 59// and DroppedAuthority.
 60type Authority interface {
 61	// Authorize executes a privileged action if the caller is authorized
 62	// Additional args can be provided for context (e.g., for proposal creation)
 63	Authorize(caller address, title string, action PrivilegedAction, args ...any) error
 64
 65	// String returns a human-readable description of the authority
 66	String() string
 67}
 68
 69// PrivilegedAction defines a function that performs a privileged action.
 70type PrivilegedAction func() error
 71
 72// PrivilegedActionHandler is called by contract-based authorities to handle
 73// privileged actions.
 74type PrivilegedActionHandler func(title string, action PrivilegedAction) error
 75
 76// NewWithCurrent creates a new Authorizer with the auth realm's address as authority
 77func NewWithCurrent() *Authorizer {
 78	return &Authorizer{
 79		auth: NewMemberAuthority(runtime.CurrentRealm().Address()),
 80	}
 81}
 82
 83// NewWithPrevious creates a new Authorizer with the previous realm's address as authority
 84func NewWithPrevious() *Authorizer {
 85	return &Authorizer{
 86		auth: NewMemberAuthority(runtime.PreviousRealm().Address()),
 87	}
 88}
 89
 90// NewWithCurrent creates a new Authorizer with the auth realm's address as authority
 91func NewWithMembers(addrs ...address) *Authorizer {
 92	return &Authorizer{
 93		auth: NewMemberAuthority(addrs...),
 94	}
 95}
 96
 97// NewWithOrigin creates a new Authorizer with the origin caller's address as
 98// authority.
 99// This is typically used in the init() function.
100func NewWithOrigin() *Authorizer {
101	origin := runtime.OriginCaller()
102	previous := runtime.PreviousRealm()
103	if origin != previous.Address() {
104		panic("NewWithOrigin() should be called from init() where runtime.PreviousRealm() is origin")
105	}
106	return &Authorizer{
107		auth: NewMemberAuthority(origin),
108	}
109}
110
111// NewWithAuthority creates a new Authorizer with a specific authority
112func NewWithAuthority(authority Authority) *Authorizer {
113	return &Authorizer{
114		auth: authority,
115	}
116}
117
118// Authority returns the auth authority implementation
119func (a *Authorizer) Authority() Authority {
120	return a.auth
121}
122
123// Transfer changes the auth authority after validation
124func (a *Authorizer) Transfer(caller address, newAuthority Authority) error {
125	// Ask auth authority to validate the transfer
126	return a.auth.Authorize(caller, "transfer_authority", func() error {
127		a.auth = newAuthority
128		return nil
129	})
130}
131
132// DoByCurrent executes a privileged action by the auth realm.
133func (a *Authorizer) DoByCurrent(title string, action PrivilegedAction, args ...any) error {
134	current := runtime.CurrentRealm()
135	caller := current.Address()
136	return a.auth.Authorize(caller, title, action, args...)
137}
138
139// DoByPrevious executes a privileged action by the previous realm.
140func (a *Authorizer) DoByPrevious(title string, action PrivilegedAction, args ...any) error {
141	previous := runtime.PreviousRealm()
142	caller := previous.Address()
143	return a.auth.Authorize(caller, title, action, args...)
144}
145
146// String returns a string representation of the auth authority
147func (a *Authorizer) String() string {
148	authStr := a.auth.String()
149
150	switch a.auth.(type) {
151	case *MemberAuthority:
152	case *ContractAuthority:
153	case *AutoAcceptAuthority:
154	case *droppedAuthority:
155	default:
156		// this way official "dropped" is different from "*custom*: dropped" (autoclaimed).
157		return ufmt.Sprintf("custom_authority[%s]", authStr)
158	}
159	return authStr
160}
161
162// MemberAuthority is the default implementation using addrset for member
163// management.
164type MemberAuthority struct {
165	members addrset.Set
166}
167
168func NewMemberAuthority(members ...address) *MemberAuthority {
169	auth := &MemberAuthority{}
170	for _, addr := range members {
171		auth.members.Add(addr)
172	}
173	return auth
174}
175
176func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
177	if !a.members.Has(caller) {
178		return errors.New("unauthorized")
179	}
180
181	if err := action(); err != nil {
182		return err
183	}
184	return nil
185}
186
187func (a *MemberAuthority) String() string {
188	addrs := []string{}
189	a.members.Tree().Iterate("", "", func(key string, _ any) bool {
190		addrs = append(addrs, key)
191		return false
192	})
193	addrsStr := strings.Join(addrs, ",")
194	return ufmt.Sprintf("member_authority[%s]", addrsStr)
195}
196
197// AddMember adds a new member to the authority
198func (a *MemberAuthority) AddMember(caller address, addr address) error {
199	return a.Authorize(caller, "add_member", func() error {
200		a.members.Add(addr)
201		return nil
202	})
203}
204
205// AddMembers adds a list of members to the authority
206func (a *MemberAuthority) AddMembers(caller address, addrs ...address) error {
207	return a.Authorize(caller, "add_members", func() error {
208		for _, addr := range addrs {
209			a.members.Add(addr)
210		}
211		return nil
212	})
213}
214
215// RemoveMember removes a member from the authority
216func (a *MemberAuthority) RemoveMember(caller address, addr address) error {
217	return a.Authorize(caller, "remove_member", func() error {
218		a.members.Remove(addr)
219		return nil
220	})
221}
222
223// Tree returns a read-only view of the members tree
224func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree {
225	tree := a.members.Tree().(*avl.Tree)
226	return rotree.Wrap(tree, nil)
227}
228
229// Has checks if the given address is a member of the authority
230func (a *MemberAuthority) Has(addr address) bool {
231	return a.members.Has(addr)
232}
233
234// ContractAuthority implements async contract-based authority
235type ContractAuthority struct {
236	contractPath    string
237	contractAddr    address
238	contractHandler PrivilegedActionHandler
239	proposer        Authority // controls who can create proposals
240}
241
242func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {
243	return &ContractAuthority{
244		contractPath:    path,
245		contractAddr:    chain.PackageAddress(path),
246		contractHandler: handler,
247		proposer:        NewAutoAcceptAuthority(), // default: anyone can propose
248	}
249}
250
251// NewRestrictedContractAuthority creates a new contract authority with a
252// proposer restriction.
253func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {
254	if path == "" {
255		panic("contract path cannot be empty")
256	}
257	if handler == nil {
258		panic("contract handler cannot be nil")
259	}
260	if proposer == nil {
261		panic("proposer cannot be nil")
262	}
263	return &ContractAuthority{
264		contractPath:    path,
265		contractAddr:    chain.PackageAddress(path),
266		contractHandler: handler,
267		proposer:        proposer,
268	}
269}
270
271func (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
272	if a.contractHandler == nil {
273		return errors.New("contract handler is not set")
274	}
275
276	// setup a once instance to ensure the action is executed only once
277	executionOnce := once.Once{}
278
279	// Wrap the action to ensure it can only be executed by the contract
280	wrappedAction := func() error {
281		current := runtime.CurrentRealm().Address()
282		if current != a.contractAddr {
283			return errors.New("action can only be executed by the contract")
284		}
285		return executionOnce.DoErr(func() error {
286			return action()
287		})
288	}
289
290	// Use the proposer authority to control who can create proposals
291	return a.proposer.Authorize(caller, title+"_proposal", func() error {
292		if err := a.contractHandler(title, wrappedAction); err != nil {
293			return err
294		}
295		return nil
296	}, args...)
297}
298
299func (a *ContractAuthority) String() string {
300	return ufmt.Sprintf("contract_authority[contract=%s]", a.contractPath)
301}
302
303// AutoAcceptAuthority implements an authority that accepts all actions
304// AutoAcceptAuthority is a simple authority that automatically accepts all
305// actions.
306// It can be used as a proposer authority to allow anyone to create proposals.
307type AutoAcceptAuthority struct{}
308
309func NewAutoAcceptAuthority() *AutoAcceptAuthority {
310	return &AutoAcceptAuthority{}
311}
312
313func (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
314	return action()
315}
316
317func (a *AutoAcceptAuthority) String() string {
318	return "auto_accept_authority"
319}
320
321// droppedAuthority implements an authority that denies all actions
322type droppedAuthority struct{}
323
324func NewDroppedAuthority() Authority {
325	return &droppedAuthority{}
326}
327
328func (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
329	return errors.New("dropped authority: all actions are denied")
330}
331
332func (a *droppedAuthority) String() string {
333	return "dropped_authority"
334}