-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add p/moul/authz (#3700)
We should probably replace most of the current Ownable-based contracts with this option. This way, we can start with a central admin and later transition to a DAO. --------- Signed-off-by: moul <[email protected]>
- Loading branch information
Showing
8 changed files
with
1,148 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
// Package authz provides flexible authorization control for privileged actions. | ||
// | ||
// # Authorization Strategies | ||
// | ||
// The package supports multiple authorization strategies: | ||
// - Member-based: Single user or team of users | ||
// - Contract-based: Async authorization (e.g., via DAO) | ||
// - Auto-accept: Allow all actions | ||
// - Drop: Deny all actions | ||
// | ||
// Core Components | ||
// | ||
// - Authority interface: Base interface implemented by all authorities | ||
// - Authorizer: Main wrapper object for authority management | ||
// - MemberAuthority: Manages authorized addresses | ||
// - ContractAuthority: Delegates to another contract | ||
// - AutoAcceptAuthority: Accepts all actions | ||
// - DroppedAuthority: Denies all actions | ||
// | ||
// Quick Start | ||
// | ||
// // Initialize with contract deployer as authority | ||
// var auth = authz.New() | ||
// | ||
// // Create functions that require authorization | ||
// func UpdateConfig(newValue string) error { | ||
// return auth.Do("update_config", func() error { | ||
// config = newValue | ||
// return nil | ||
// }) | ||
// } | ||
// | ||
// See example_test.gno for more usage examples. | ||
package authz | ||
|
||
import ( | ||
"errors" | ||
"std" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/avl/rotree" | ||
"gno.land/p/demo/ufmt" | ||
"gno.land/p/moul/addrset" | ||
"gno.land/p/moul/once" | ||
) | ||
|
||
// Authorizer is the main wrapper object that handles authority management | ||
type Authorizer struct { | ||
current Authority | ||
} | ||
|
||
// Authority represents an entity that can authorize privileged actions | ||
type Authority interface { | ||
// Authorize executes a privileged action if the caller is authorized | ||
// Additional args can be provided for context (e.g., for proposal creation) | ||
Authorize(title string, action PrivilegedAction, args ...interface{}) error | ||
|
||
// String returns a human-readable description of the authority | ||
String() string | ||
} | ||
|
||
// PrivilegedAction defines a function that performs a privileged action. | ||
type PrivilegedAction func() error | ||
|
||
// PrivilegedActionHandler is called by contract-based authorities to handle | ||
// privileged actions. | ||
type PrivilegedActionHandler func(title string, action PrivilegedAction) error | ||
|
||
// New creates a new Authorizer with the current realm's address as authority | ||
func New() *Authorizer { | ||
return &Authorizer{ | ||
current: NewMemberAuthority(std.PreviousRealm().Address()), | ||
} | ||
} | ||
|
||
// NewWithAuthority creates a new Authorizer with a specific authority | ||
func NewWithAuthority(authority Authority) *Authorizer { | ||
return &Authorizer{ | ||
current: authority, | ||
} | ||
} | ||
|
||
// Current returns the current authority implementation | ||
func (a *Authorizer) Current() Authority { | ||
return a.current | ||
} | ||
|
||
// Transfer changes the current authority after validation | ||
func (a *Authorizer) Transfer(newAuthority Authority) error { | ||
// Ask current authority to validate the transfer | ||
return a.current.Authorize("transfer_authority", func() error { | ||
a.current = newAuthority | ||
return nil | ||
}) | ||
} | ||
|
||
// Do executes a privileged action through the current authority | ||
func (a *Authorizer) Do(title string, action PrivilegedAction, args ...interface{}) error { | ||
return a.current.Authorize(title, action, args...) | ||
} | ||
|
||
// String returns a string representation of the current authority | ||
func (a *Authorizer) String() string { | ||
return a.current.String() | ||
} | ||
|
||
// MemberAuthority is the default implementation using addrset for member management | ||
type MemberAuthority struct { | ||
members addrset.Set | ||
} | ||
|
||
func NewMemberAuthority(members ...std.Address) *MemberAuthority { | ||
auth := &MemberAuthority{} | ||
for _, addr := range members { | ||
auth.members.Add(addr) | ||
} | ||
return auth | ||
} | ||
|
||
func (a *MemberAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { | ||
caller := std.PreviousRealm().Address() | ||
if !a.members.Has(caller) { | ||
return errors.New("unauthorized") | ||
} | ||
|
||
if err := action(); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (a *MemberAuthority) String() string { | ||
return ufmt.Sprintf("member_authority[size=%d]", a.members.Size()) | ||
} | ||
|
||
// AddMember adds a new member to the authority | ||
func (a *MemberAuthority) AddMember(addr std.Address) error { | ||
return a.Authorize("add_member", func() error { | ||
a.members.Add(addr) | ||
return nil | ||
}) | ||
} | ||
|
||
// RemoveMember removes a member from the authority | ||
func (a *MemberAuthority) RemoveMember(addr std.Address) error { | ||
return a.Authorize("remove_member", func() error { | ||
a.members.Remove(addr) | ||
return nil | ||
}) | ||
} | ||
|
||
// Tree returns a read-only view of the members tree | ||
func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree { | ||
tree := a.members.Tree().(*avl.Tree) | ||
return rotree.Wrap(tree, nil) | ||
} | ||
|
||
// Has checks if the given address is a member of the authority | ||
func (a *MemberAuthority) Has(addr std.Address) bool { | ||
return a.members.Has(addr) | ||
} | ||
|
||
// ContractAuthority implements async contract-based authority | ||
type ContractAuthority struct { | ||
contractPath string | ||
contractAddr std.Address | ||
contractHandler PrivilegedActionHandler | ||
proposer Authority // controls who can create proposals | ||
executionOnce once.Once | ||
} | ||
|
||
func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority { | ||
return &ContractAuthority{ | ||
contractPath: path, | ||
contractAddr: std.DerivePkgAddr(path), | ||
contractHandler: handler, | ||
proposer: NewAutoAcceptAuthority(), // default: anyone can propose | ||
executionOnce: once.Once{}, // initialize execution once | ||
} | ||
} | ||
|
||
// NewRestrictedContractAuthority creates a new contract authority with a proposer restriction | ||
func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority { | ||
if path == "" { | ||
panic("contract path cannot be empty") | ||
} | ||
if handler == nil { | ||
panic("contract handler cannot be nil") | ||
} | ||
if proposer == nil { | ||
panic("proposer cannot be nil") | ||
} | ||
return &ContractAuthority{ | ||
contractPath: path, | ||
contractAddr: std.DerivePkgAddr(path), | ||
contractHandler: handler, | ||
proposer: proposer, | ||
executionOnce: once.Once{}, | ||
} | ||
} | ||
|
||
func (a *ContractAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { | ||
if a.contractHandler == nil { | ||
return errors.New("contract handler is not set") | ||
} | ||
|
||
// Wrap the action to ensure it can only be executed by the contract | ||
wrappedAction := func() error { | ||
caller := std.PreviousRealm().Address() | ||
if caller != a.contractAddr { | ||
return errors.New("action can only be executed by the contract") | ||
} | ||
return a.executionOnce.DoErr(func() error { | ||
return action() | ||
}) | ||
} | ||
|
||
// Use the proposer authority to control who can create proposals | ||
return a.proposer.Authorize(title+"_proposal", func() error { | ||
if err := a.contractHandler(title, wrappedAction); err != nil { | ||
return err | ||
} | ||
return nil | ||
}, args...) | ||
} | ||
|
||
func (a *ContractAuthority) String() string { | ||
return ufmt.Sprintf("contract_authority[contract=%s]", a.contractPath) | ||
} | ||
|
||
// AutoAcceptAuthority implements an authority that accepts all actions | ||
// AutoAcceptAuthority is a simple authority that automatically accepts all actions. | ||
// It can be used as a proposer authority to allow anyone to create proposals. | ||
type AutoAcceptAuthority struct{} | ||
|
||
func NewAutoAcceptAuthority() *AutoAcceptAuthority { | ||
return &AutoAcceptAuthority{} | ||
} | ||
|
||
func (a *AutoAcceptAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { | ||
return action() | ||
} | ||
|
||
func (a *AutoAcceptAuthority) String() string { | ||
return "auto_accept_authority" | ||
} | ||
|
||
// droppedAuthority implements an authority that denies all actions | ||
type droppedAuthority struct{} | ||
|
||
func NewDroppedAuthority() Authority { | ||
return &droppedAuthority{} | ||
} | ||
|
||
func (a *droppedAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { | ||
return errors.New("dropped authority: all actions are denied") | ||
} | ||
|
||
func (a *droppedAuthority) String() string { | ||
return "dropped_authority" | ||
} |
Oops, something went wrong.