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}