Pure package detail
authz
gno.land/p/moul/authz/v1
Indexed deployment identity with independently loaded latest RPC source. Functions and Render are realm-only RPC capabilities.
Indexed deployment
Identity
- Package path
- gno.land/p/moul/authz/v1
- Block
- 23349
- Deployed (UTC)
- Transaction
- gbnVJzHGZcrGZUdnD7T6Nm/RPzgHsyjOskT8GzExUUg=
Latest RPC state
Source
// 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 member address(...)
// var auth = authz.NewWithMembers(member)
//
// // Create functions that require authorization
// func UpdateConfig(cur realm, newValue string) error {
// return auth.DoByPrevious(0, cur, "update_config", func() error {
// config = newValue
// return nil
// })
// }
//
// See example_test.gno for more usage examples.
package authz
import (
"chain"
"errors"
"strings"
"gno.land/p/moul/addrset/v1"
"gno.land/p/moul/once/v1"
"gno.land/p/nt/avl/v0"
"gno.land/p/nt/avl/v0/rotree"
"gno.land/p/nt/ufmt/v0"
)
// Authorizer is the main wrapper object that handles authority management.
// It is configured with a replaceable Authority implementation.
type Authorizer struct {
auth Authority
}
// Authority represents an entity that can authorize privileged actions.
// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,
// and DroppedAuthority.
//
// Authority is the canonical safe shape for cross-package authority
// interfaces: methods are address-typed (no realm/cur crosses the interface
// boundary), and consumers correctly derive `caller` from
// `cur.Previous().Address()` under `rlm.IsCurrent()` before invoking
// Authorize. No cur-leak (class 1) is possible through this interface.
//
// However, two RESIDUAL RISKS apply:
//
// - Class-3 impl-substitution: NewWithAuthority and Authorizer.Transfer
// accept any Authority impl. A malicious Authority can always-approve
// (silent privilege escalation) or always-deny (denial-of-service).
// Consumers should pass canonical impls from this package
// (MemberAuthority, ContractAuthority, AutoAcceptAuthority,
// DroppedAuthority) unless they have explicit reason to register a
// foreign impl. We do not expose an IsCanonicalAuthority allowlist
// because the package is intentionally extensible — third-party impls
// are the design intent.
//
// - Class-4 closed-over-authority: NewContractAuthority and
// NewRestrictedContractAuthority capture a caller-supplied
// PrivilegedActionHandler closure. The handler runs synchronously
// inside Authorize with the consumer's authority. A hostile handler
// can swallow actions, log the caller, or execute arbitrary code
// under the consumer's frame. Register only trusted handler functions.
// See r/gnops/valopers/init.gno for the realistic registration shape.
//
// We do NOT seal Authority via an unexported marker method — that pattern
// is bypassable via embedding in Gno; see
// p/test/seal/filetests/z_seal_*_filetest.gno for the four bypass tests.
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(caller address, title string, action PrivilegedAction, args ...any) 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
// NewWithMembers creates a new Authorizer whose authority is a
// MemberAuthority containing the given addresses. Callers express
// authority intent at the call site:
//
// // "auth realm is the authority"
// a := authz.NewWithMembers(cur.Address())
//
// // "previous realm is the authority" (from a crossing function)
// a := authz.NewWithMembers(cur.Previous().Address())
//
// // "EOA caller is the authority" (from init(cur realm))
// if !cur.Previous().IsUserCall() {
// panic("realm must be initialized by EOA")
// }
// a := authz.NewWithMembers(cur.Previous().Address())
//
// This replaces the previous NewWithCurrent / NewWithPrevious /
// NewWithOrigin sugar — those baked runtime.{Current,Previous,Origin}
// reads into the constructor, which (a) prevented use from package-
// level var initializers, (b) made the EOA-origin check inside
// NewWithOrigin an indirect address comparison rather than the
// straightforward IsUserCall predicate, and (c) coupled the
// constructor to the runtime walks the rest of the migration is
// moving away from.
func NewWithMembers(addrs ...address) *Authorizer {
return &Authorizer{
auth: NewMemberAuthority(addrs...),
}
}
// NewWithAuthority creates a new Authorizer with a specific authority.
//
// SECURITY: `authority` is an open-interface input — any value satisfying
// Authority is accepted. A malicious impl can always-approve (privilege
// escalation) or always-deny (DoS). Prefer canonical impls from this
// package (NewMemberAuthority, NewContractAuthority, NewAutoAcceptAuthority,
// NewDroppedAuthority) unless you specifically need a foreign impl.
func NewWithAuthority(authority Authority) *Authorizer {
return &Authorizer{
auth: authority,
}
}
// Authority returns the auth authority implementation
func (a *Authorizer) Authority() Authority {
return a.auth
}
// Transfer changes the auth authority after validation. rlm must be the
// caller's own captured cur (asserted via rlm.IsCurrent()); the
// principal is rlm.Previous().Address(). Closes the address-parameter
// forgery: an external realm cannot supply Owner() as `caller` to
// bypass the underlying Authority's check.
//
// SECURITY (runtime substitution): once the current authority approves a
// Transfer, the new authority is installed and effective on the next call.
// If an attacker ever becomes the authority — even briefly — they can
// install a permanent DroppedAuthority (DoS) or an AutoAcceptAuthority
// (privilege escalation). Consumers concerned about this should wrap
// Transfer with a one-shot guard or a quorum/cooldown check.
//
// `newAuthority` is also an open-interface input — see NewWithAuthority's
// Class-3 caveat. Pass canonical impls.
func (a *Authorizer) Transfer(_ int, rlm realm, newAuthority Authority) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
caller := rlm.Previous().Address()
return a.auth.Authorize(caller, "transfer_authority", func() error {
a.auth = newAuthority
return nil
})
}
// DoByCurrent executes a privileged action authorized as `rlm`. `rlm`
// must be the caller's own live cur (asserted via rlm.IsCurrent());
// the authorized principal is `rlm.Address()`. To authorize as the
// realm that called your function, use `DoByPrevious`.
//
// auth.DoByCurrent(0, cur, "update_config", func() error { ... }) // current realm authorizes
// auth.DoByPrevious(0, cur, "update_config", func() error { ... }) // calling realm authorizes
//
// The `_ int` first parameter is a deliberate sentinel that pushes
// `rlm realm` past the first-arg position so DoByCurrent stays a
// non-crossing method — otherwise it would be a crossing method and
// rlm.Previous() inside would resolve one realm deeper than the caller
// intended.
//
// SECURITY: the IsCurrent guard closes Class-2 designation forgery (see
// docs/resources/gno-security.md). A realm value's .Address() is set
// when the value is minted at a crossing frame; the value can in
// principle be stored and replayed. Without IsCurrent, a hostile realm
// could capture a high-privilege realm's cur.Previous() (e.g., when
// that realm called into it) and later pass the stored value here to
// authorize actions as that realm. IsCurrent rejects stale captures by
// requiring the value to match the topmost live crossing frame's cur.
func (a *Authorizer) DoByCurrent(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
return a.auth.Authorize(rlm.Address(), title, action, args...)
}
// DoByPrevious executes a privileged action authorized as the realm
// that called the function invoking DoByPrevious. `rlm` must be the
// caller's own live cur; the principal is derived as
// `rlm.Previous().Address()`. Mirrors the Transfer/AddMember pattern:
// always take live cur, derive the caller-of-caller internally rather
// than accepting a stored/forwarded realm value.
func (a *Authorizer) DoByPrevious(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
return a.auth.Authorize(rlm.Previous().Address(), title, action, args...)
}
// String returns a string representation of the auth authority
func (a *Authorizer) String() string {
authStr := a.auth.String()
switch a.auth.(type) {
case *MemberAuthority:
case *ContractAuthority:
case *AutoAcceptAuthority:
case *droppedAuthority:
default:
// this way official "dropped" is different from "*custom*: dropped" (autoclaimed).
return ufmt.Sprintf("custom_authority[%s]", authStr)
}
return authStr
}
// MemberAuthority is the default implementation using addrset for member
// management.
type MemberAuthority struct {
members addrset.Set
}
func NewMemberAuthority(members ...address) *MemberAuthority {
auth := &MemberAuthority{}
for _, addr := range members {
auth.members.Add(addr)
}
return auth
}
func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
if !a.members.Has(caller) {
return errors.New("unauthorized")
}
if err := action(); err != nil {
return err
}
return nil
}
func (a *MemberAuthority) String() string {
addrs := []string{}
a.members.Tree().Iterate("", "", func(key string, _ any) bool {
addrs = append(addrs, key)
return false
})
addrsStr := strings.Join(addrs, ",")
return ufmt.Sprintf("member_authority[%s]", addrsStr)
}
// AddMember adds a new member to the authority. rlm must be the caller's
// own captured cur; the principal is rlm.Previous().Address() and must
// already be a member. The IsCurrent guard closes the forgery where an
// external realm passes Owner() as caller to bypass members.Has(caller).
func (a *MemberAuthority) AddMember(_ int, rlm realm, addr address) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
caller := rlm.Previous().Address()
return a.Authorize(caller, "add_member", func() error {
a.members.Add(addr)
return nil
})
}
// AddMembers adds a list of members to the authority. Same rlm contract
// as AddMember.
func (a *MemberAuthority) AddMembers(_ int, rlm realm, addrs ...address) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
caller := rlm.Previous().Address()
return a.Authorize(caller, "add_members", func() error {
for _, addr := range addrs {
a.members.Add(addr)
}
return nil
})
}
// RemoveMember removes a member from the authority. Same rlm contract
// as AddMember.
func (a *MemberAuthority) RemoveMember(_ int, rlm realm, addr address) error {
if !rlm.IsCurrent() {
return errors.New("unauthorized")
}
caller := rlm.Previous().Address()
return a.Authorize(caller, "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 address) bool {
return a.members.Has(addr)
}
// ContractAuthority implements async contract-based authority
type ContractAuthority struct {
contractPath string
contractAddr address
contractHandler PrivilegedActionHandler
proposer Authority // controls who can create proposals
}
// NewContractAuthority creates a new contract-based authority.
//
// SECURITY (Class-4 captured callback): `handler` is a caller-supplied
// closure that runs SYNCHRONOUSLY inside Authorize, with the consumer's
// authority. A hostile handler can swallow actions, log the caller, or
// execute arbitrary code under the consumer's frame. The package-internal
// wrappedAction guards "execute action only from contractAddr" but the
// handler can call wrappedAction however it likes (multiple times, never,
// out of order). Register only trusted handler functions; treat handler
// registration as the trust boundary.
func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {
return &ContractAuthority{
contractPath: path,
contractAddr: chain.PackageAddress(path),
contractHandler: handler,
proposer: NewAutoAcceptAuthority(), // default: anyone can propose
}
}
// NewRestrictedContractAuthority creates a new contract authority with a
// proposer restriction.
//
// SECURITY:
// - `handler` is the same Class-4 captured-callback risk as
// NewContractAuthority — runs synchronously inside Authorize with the
// consumer's authority. Register only trusted handler functions.
// - `proposer` is an open-interface input (Class-3 impl-substitution).
// A hostile proposer Authority can always-approve creation of any
// proposal, defeating the restriction. Pass canonical impls only.
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: chain.PackageAddress(path),
contractHandler: handler,
proposer: proposer,
}
}
func (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
if a.contractHandler == nil {
return errors.New("contract handler is not set")
}
// setup a once instance to ensure the action is executed only once
executionOnce := once.Once{}
// wrappedAction enforces at-most-once invocation. The previous
// gate `unsafe.CurrentRealm() == contractAddr` is removed: it
// was .Title()-bypassable (runtime.CurrentRealm walks past
// non-crossing frames to the most-recent crossing ancestor) and
// the trust boundary is now upstream — Authorizer.DoByCurrent /
// DoByPrevious require rlm.IsCurrent() and pass a non-forgeable
// principal to Authorize, while the consumer realm's handler
// closure is the Class-4 trust root by lexical capture at
// registration time.
wrappedAction := func() error {
return executionOnce.DoErr(func() error {
return action()
})
}
// Use the proposer authority to control who can create proposals
return a.proposer.Authorize(caller, 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(caller address, title string, action PrivilegedAction, args ...any) 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(caller address, title string, action PrivilegedAction, args ...any) error {
return errors.New("dropped authority: all actions are denied")
}
func (a *droppedAuthority) String() string {
return "dropped_authority"
}
The verified vm/qfuncs operation accepts realm paths only.
Pure packages expose source files but do not have Realm Render.