Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package authz provides flexible authorization control for privileged actions.

Overview

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: The contract at a given path is itself the authority
  • 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: Makes the contract at a path its own authority
  • AutoAcceptAuthority: Accepts all actions
  • DroppedAuthority: Denies all actions

Quick Start

Example
 1// Initialize with contract deployer as authority
 2var member address(...)
 3var auth = authz.NewWithMembers(member)
 4
 5// Create functions that require authorization
 6func UpdateConfig(cur realm, newValue string) error {
 7	return auth.DoByPrevious(0, cur, "update_config", func() error {
 8		config = newValue
 9		return nil
10	})
11}

See example_test.gno for more usage examples.

Functions 7

func NewRestrictedContractAuthority

1func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority
source

NewRestrictedContractAuthority creates a contract authority whose proposer REPLACES the contract-identity gate.

This is the escape hatch, not a second lock. `proposer` becomes the ONLY authorization decision: contractAddr is not consulted on this path, so `path` degrades to a label that appears in String() and asserts nothing. NewRestrictedContractAuthority(p, h, NewAutoAcceptAuthority()) is exactly the old default behaviour ("anyone can propose"), stated explicitly rather than reached by default.

In particular NewRestrictedContractAuthority("gno.land/r/gov/dao", h, NewMemberAuthority(alice)) does NOT mean "GovDAO and alice". It means "alice, and not GovDAO".

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. String() renders a non-canonical proposer wrapped as custom_authority[...] so such a substitution is at least visible.

func NewWithAuthority

1func NewWithAuthority(authority Authority) *Authorizer
source

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 NewWithMembers

1func NewWithMembers(addrs ...address) *Authorizer
source

NewWithMembers creates a new Authorizer whose authority is a MemberAuthority containing the given addresses. Callers express authority intent at the call site:

Example
 1// "auth realm is the authority"
 2a := authz.NewWithMembers(cur.Address())
 3
 4// "previous realm is the authority" (from a crossing function)
 5a := authz.NewWithMembers(cur.Previous().Address())
 6
 7// "EOA caller is the authority" (from init(cur realm))
 8if !cur.Previous().IsUserCall() {
 9    panic("realm must be initialized by EOA")
10}
11a := 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 NewContractAuthority

1func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority
source

NewContractAuthority makes the contract at `path` its OWN authority: the default proposer accepts only chain.PackageAddress(path), so a privileged action proceeds only when driven from that contract's own frame.

NewRestrictedContractAuthority does NOT widen this — it REPLACES it. See its godoc; with an explicit proposer, `path` is a label and contractAddr is never consulted.

The `caller` compared against that address is established upstream by Authorizer.DoByCurrent / DoByPrevious / Transfer under rlm.IsCurrent(), so an external realm cannot present the contract address.

BREAKING: the default proposer used to be NewAutoAcceptAuthority ("anyone can propose"). Consumers that relied on arbitrary callers driving a plain ContractAuthority will start getting "unauthorized"; that is the fix, not a regression. Pass an explicit proposer to NewRestrictedContractAuthority to opt back in.

SECURITY — the gate binds the contract's IDENTITY, not its intent. Inside any crossing frame of the contract, rlm.Address() is unconditionally PackageAddress(path), so `DoByCurrent` from within the contract is a tautology; only DoByPrevious and Transfer gain a real check. Consequently ANY exported function of the contract that returns a crossing closure (or anything else carrying its frame) hands this authority to its caller. Keep privileged closures unexported.

So DO NOT write NewContractAuthority(ownPath) + DoByCurrent. It reads like "only I may do this" and compiles to "anyone may do this". Point `path` at the principal you actually want to assert — usually the governance realm that will drive the action — and use DoByPrevious, so the comparison is against whoever crossed in:

Example
1// in r/gnops/valopers/init.gno
2auth = authz.NewWithAuthority(
3    authz.NewContractAuthority("gno.land/r/gov/dao", handler),
4)
5// in the privileged write
6auth.DoByPrevious(0, rlm, "update-instructions", action)

Own-path is right only when something OTHER than the contract's own frame supplies the caller — i.e. you drive it via DoByPrevious from a realm you have deliberately let call in, or via Transfer.

SECURITY — a gate is only as good as the principal it names. Pointing `path` at a realm whose frame ANY caller can mint buys nothing. Check, for whatever principal you choose, that nothing exported by that realm hands out its frame identity:

  • `gno.land/r/gov/dao` is safe to assert ONLY because SimpleExecutor.Execute rejects invocation from outside the proxy (r/gov/dao/types.gno). Before that gate existed, dao's exported NewSimpleExecutor + Execute let any realm mint a frame whose Previous() was r/gov/dao, so this whole pairing authenticated nothing. Do not assume a governance path is unforgeable; verify it.
  • Whatever the principal, an exported function of YOUR realm whose signature is assignable to a callback type the principal invokes (for r/gov/dao: `func(realm) error`) is itself the leak — the principal will happily call it for an attacker. Keep privileged entrypoints out of that shape, or take extra parameters.

Neither shape defends against re-exporting the privileged closure: its holder picks the Previous() they present by wrapping it in an executor of their own. Unexported closures remain the load-bearing rule.

A syntactically valid but WRONG `path` is a permanent brick, not an error: the authority accepts nobody, and Transfer routes through the same gate, so it cannot be rotated out either. Construction rejects malformed paths, but it cannot tell a wrong path from a right one — e.g. "gno.land/r/gov/dao/impl/v0" (the path r/gov/dao's own allowedDAOs list holds) is well-formed and permanently dead, because an executor's Previous() is the PROXY path, never the impl. Give the consumer a governance-gated rotation entrypoint before deploying; see r/gnops/valopers/admin.gno's NewAuthorityRotationProposalRequest.

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 enforces at-most-once invocation and NOTHING ELSE — in particular it does not check the caller, and the handler may call it however it likes (multiple times, never, out of order). Register only trusted handler functions; treat handler registration as the trust boundary.

Panics if `path` is empty or malformed, or if `handler` is nil.

Types 7

type Authority

interface
1type Authority interface {
2	// Authorize executes a privileged action if the caller is authorized
3	// Additional args can be provided for context (e.g., for proposal creation)
4	Authorize(caller address, title string, action PrivilegedAction, args ...any) error
5
6	// String returns a human-readable description of the authority
7	String() string
8}
source

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.

  • Caller- and title-forgery on the RAW interface: Authorize takes `caller` and `title` as ARGUMENTS, not from the frame. A consumer that exposes a bare Authority (rather than the *Authorizer that wraps it) therefore lets any holder present any caller and any title. Caller-forgery is contained — the only authority-mutating closure lives inside Authorizer.Transfer, so a forged caller on a raw Authorize runs the caller's own inert closure and cannot transfer; TestForgedCallerCannotTransfer pins that boundary. Title-forgery is NOT contained: the title is passed straight to the contractHandler, so a handler that branches on it (routing, quotas, audit trails) must not treat it as trusted. Keep the Authority unexported and hand out only what callers need; see r/gnops/valopers/admin.gno, which exports a description string.

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 Authorizer

struct
1type Authorizer struct {
2	auth Authority
3}
source

Authorizer is the main wrapper object that handles authority management. It is configured with a replaceable Authority implementation.

Methods on Authorizer

func Authority

method on Authorizer
1func (a *Authorizer) Authority() Authority
source

Authority returns the auth authority implementation

func DoByCurrent

method on Authorizer
1func (a *Authorizer) DoByCurrent(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error
source

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`.

Example
1auth.DoByCurrent(0, cur, "update_config", func() error { ... })    // current realm authorizes
2auth.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 DoByPrevious

method on Authorizer
1func (a *Authorizer) DoByPrevious(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error
source

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 String

method on Authorizer
1func (a *Authorizer) String() string
source

String returns a string representation of the auth authority.

A non-canonical impl is wrapped as custom_authority[...] so that the official "dropped" is distinguishable from a "*custom*: dropped" (autoclaimed) one. Shared with ContractAuthority.String, which applies the same rule to its nested proposer.

func Transfer

method on Authorizer
1func (a *Authorizer) Transfer(_ int, rlm realm, newAuthority Authority) error
source

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.

type AutoAcceptAuthority

struct
1type AutoAcceptAuthority struct{}
source

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.

Methods on AutoAcceptAuthority

func Authorize

method on AutoAcceptAuthority
1func (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error
source

func String

method on AutoAcceptAuthority
1func (a *AutoAcceptAuthority) String() string
source

type ContractAuthority

struct
1type ContractAuthority struct {
2	contractPath    string
3	contractAddr    address
4	contractHandler PrivilegedActionHandler
5	proposer        Authority // controls who can create proposals
6}
source

ContractAuthority implements async contract-based authority

Methods on ContractAuthority

func Authorize

method on ContractAuthority
1func (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error
source

func String

method on ContractAuthority
1func (a *ContractAuthority) String() string
source

String renders the contract path AND the proposer.

The proposer half is security-relevant, not cosmetic: it is the only thing distinguishing a gated authority from a wide-open one. Rendering the path alone made

Example
1NewContractAuthority(path, handler)                              // gated
2NewRestrictedContractAuthority(path, handler, AutoAccept{})      // open to all

byte-identical, so any consumer test asserting on this string — and any on-chain reader inspecting it — was blind to the difference. A consumer realm could have its authority swapped for a fully permissive one and its assertions would stay green.

"contract-identity" names the default installed by NewContractAuthority: the contract at contractPath, and nobody else, may drive this authority.

The proposer is rendered through canonicalAuthorityString, NOT by calling a.proposer.String() directly. Authority is an open interface, so a foreign impl can return any text it likes — including "contract-identity". That made a fully permissive authority byte-identical to the gated default again, defeating the very assertions this rendering exists to support. Non-canonical impls are wrapped as custom_authority[...], mirroring what Authorizer.String has always done at the outer level.

Read the `contract=` half with care: it is load-bearing only for the contract-identity default. With any other proposer it is a label — see NewRestrictedContractAuthority.

type MemberAuthority

struct
1type MemberAuthority struct {
2	members addrset.Set
3}
source

MemberAuthority is the default implementation using addrset for member management.

Methods on MemberAuthority

func AddMember

method on MemberAuthority
1func (a *MemberAuthority) AddMember(_ int, rlm realm, addr address) error
source

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 AddMembers

method on MemberAuthority
1func (a *MemberAuthority) AddMembers(_ int, rlm realm, addrs ...address) error
source

AddMembers adds a list of members to the authority. Same rlm contract as AddMember.

func Authorize

method on MemberAuthority
1func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error
source

func Has

method on MemberAuthority
1func (a *MemberAuthority) Has(addr address) bool
source

Has checks if the given address is a member of the authority

func RemoveMember

method on MemberAuthority
1func (a *MemberAuthority) RemoveMember(_ int, rlm realm, addr address) error
source

RemoveMember removes a member from the authority. Same rlm contract as AddMember.

func String

method on MemberAuthority
1func (a *MemberAuthority) String() string
source

func Tree

method on MemberAuthority
1func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree
source

Tree returns a read-only view of the members tree

type PrivilegedAction

func
1type PrivilegedAction func() error
source

PrivilegedAction defines a function that performs a privileged action.

type PrivilegedActionHandler

func
1type PrivilegedActionHandler func(title string, action PrivilegedAction) error
source

PrivilegedActionHandler is called by contract-based authorities to handle privileged actions.

Imports 8

Source Files 5