authz.gno

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