Skip to content

Commit

Permalink
feat(examples): add p/moul/authz (#3700)
Browse files Browse the repository at this point in the history
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
moul authored Feb 24, 2025
1 parent 6803b77 commit 83c83f9
Show file tree
Hide file tree
Showing 8 changed files with 1,148 additions and 0 deletions.
261 changes: 261 additions & 0 deletions examples/gno.land/p/moul/authz/authz.gno
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"
}
Loading

0 comments on commit 83c83f9

Please sign in to comment.