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}