PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/authz/v1

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v1
Namespace
moul / authz
Files
3 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/authz/v1
gno
0.9

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • authz.gnogno
authz.gnogno
1// Package authz provides flexible authorization control for privileged actions.2//3// # Authorization Strategies4//5// The package supports multiple authorization strategies:6//   - Member-based: Single user or team of users7//   - Contract-based: The contract at a given path is itself the authority8//   - Auto-accept: Allow all actions9//   - Drop: Deny all actions10//11// Core Components12//13//   - Authority interface: Base interface implemented by all authorities14//   - Authorizer: Main wrapper object for authority management15//   - MemberAuthority: Manages authorized addresses16//   - ContractAuthority: Makes the contract at a path its own authority17//   - AutoAcceptAuthority: Accepts all actions18//   - DroppedAuthority: Denies all actions19//20// Quick Start21//22//	// Initialize with contract deployer as authority23//	var member address(...)24//	var auth = authz.NewWithMembers(member)25//26//	// Create functions that require authorization27//	func UpdateConfig(cur realm, newValue string) error {28//		return auth.DoByPrevious(0, cur, "update_config", func() error {29//			config = newValue30//			return nil31//		})32//	}33//34// See example_test.gno for more usage examples.35package authz3637import (38	"chain"39	"errors"40	"strings"4142	"gno.land/p/moul/addrset/v1"43	"gno.land/p/moul/once/v0"44	"gno.land/p/nt/ufmt/v0"45)4647// Authorizer is the main wrapper object that handles authority management.48// It is configured with a replaceable Authority implementation.49type Authorizer struct {50	auth Authority51}5253// Authority represents an entity that can authorize privileged actions.54// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,55// and DroppedAuthority.56//57// Authority is the canonical safe shape for cross-package authority58// interfaces: methods are address-typed (no realm/cur crosses the interface59// boundary), and consumers correctly derive `caller` from60// `cur.Previous().Address()` under `rlm.IsCurrent()` before invoking61// Authorize. No cur-leak (class 1) is possible through this interface.62//63// However, two RESIDUAL RISKS apply:64//65//   - Class-3 impl-substitution: NewWithAuthority and Authorizer.Transfer66//     accept any Authority impl. A malicious Authority can always-approve67//     (silent privilege escalation) or always-deny (denial-of-service).68//     Consumers should pass canonical impls from this package69//     (MemberAuthority, ContractAuthority, AutoAcceptAuthority,70//     DroppedAuthority) unless they have explicit reason to register a71//     foreign impl. We do not expose an IsCanonicalAuthority allowlist72//     because the package is intentionally extensible — third-party impls73//     are the design intent.74//75//   - Class-4 closed-over-authority: NewContractAuthority and76//     NewRestrictedContractAuthority capture a caller-supplied77//     PrivilegedActionHandler closure. The handler runs synchronously78//     inside Authorize with the consumer's authority. A hostile handler79//     can swallow actions, log the caller, or execute arbitrary code80//     under the consumer's frame. Register only trusted handler functions.81//     See r/gnops/valopers/init.gno for the realistic registration shape.82//83//   - Caller- and title-forgery on the RAW interface: Authorize takes84//     `caller` and `title` as ARGUMENTS, not from the frame. A consumer85//     that exposes a bare Authority (rather than the *Authorizer that86//     wraps it) therefore lets any holder present any caller and any87//     title. Caller-forgery is contained — the only authority-mutating88//     closure lives inside Authorizer.Transfer, so a forged caller on a89//     raw Authorize runs the caller's own inert closure and cannot90//     transfer; TestForgedCallerCannotTransfer pins that boundary.91//     Title-forgery is NOT contained: the title is passed straight to the92//     contractHandler, so a handler that branches on it (routing, quotas,93//     audit trails) must not treat it as trusted. Keep the Authority94//     unexported and hand out only what callers need; see95//     r/gnops/valopers/admin.gno, which exports a description string.96//97// # Backing store98//99// This is the B+ tree successor to [gno.land/p/moul/authz/v0] (which is100// backed by an AVL tree via gno.land/p/moul/addrset/v0): a bump to v1101// because the member store — and thus the on-chain storage layout —102// changed to gno.land/p/moul/addrset/v1, itself backed by103// gno.land/p/nt/bptree/v0. This is a storage/compatibility change; the104// exported API is otherwise the same as v0, EXCEPT that the v0105// MemberAuthority.Tree() escape hatch is intentionally removed so the106// backing store never leaks across realms.107//108// Two behavioral caveats follow from the in-place-mutating B+ tree backing:109//110//   - do NOT mutate a MemberAuthority (AddMember/RemoveMember) from inside111//     an iteration callback — the AVL backing's copy-on-write tolerated it,112//     this one does not;113//   - do NOT copy a non-zero MemberAuthority (or its members set) by value —114//     the copies would share live tree nodes while their roots and sizes115//     diverge (v0's copies were independent snapshots).116//117// We do NOT seal Authority via an unexported marker method — that pattern118// is bypassable via embedding in Gno; see119// p/test/seal/filetests/z_seal_*_filetest.gno for the four bypass tests.120type Authority interface {121	// Authorize executes a privileged action if the caller is authorized122	// Additional args can be provided for context (e.g., for proposal creation)123	Authorize(caller address, title string, action PrivilegedAction, args ...any) error124125	// String returns a human-readable description of the authority126	String() string127}128129// PrivilegedAction defines a function that performs a privileged action.130type PrivilegedAction func() error131132// PrivilegedActionHandler is called by contract-based authorities to handle133// privileged actions.134type PrivilegedActionHandler func(title string, action PrivilegedAction) error135136// NewWithMembers creates a new Authorizer whose authority is a137// MemberAuthority containing the given addresses. Callers express138// authority intent at the call site:139//140//	// "auth realm is the authority"141//	a := authz.NewWithMembers(cur.Address())142//143//	// "previous realm is the authority" (from a crossing function)144//	a := authz.NewWithMembers(cur.Previous().Address())145//146//	// "EOA caller is the authority" (from init(cur realm))147//	if !cur.Previous().IsUserCall() {148//	    panic("realm must be initialized by EOA")149//	}150//	a := authz.NewWithMembers(cur.Previous().Address())151//152// This replaces the previous NewWithCurrent / NewWithPrevious /153// NewWithOrigin sugar — those baked runtime.{Current,Previous,Origin}154// reads into the constructor, which (a) prevented use from package-155// level var initializers, (b) made the EOA-origin check inside156// NewWithOrigin an indirect address comparison rather than the157// straightforward IsUserCall predicate, and (c) coupled the158// constructor to the runtime walks the rest of the migration is159// moving away from.160func NewWithMembers(addrs ...address) *Authorizer {161	return &Authorizer{162		auth: NewMemberAuthority(addrs...),163	}164}165166// NewWithAuthority creates a new Authorizer with a specific authority.167//168// SECURITY: `authority` is an open-interface input — any value satisfying169// Authority is accepted. A malicious impl can always-approve (privilege170// escalation) or always-deny (DoS). Prefer canonical impls from this171// package (NewMemberAuthority, NewContractAuthority, NewAutoAcceptAuthority,172// NewDroppedAuthority) unless you specifically need a foreign impl.173func NewWithAuthority(authority Authority) *Authorizer {174	return &Authorizer{175		auth: authority,176	}177}178179// Authority returns the auth authority implementation180func (a *Authorizer) Authority() Authority {181	return a.auth182}183184// Transfer changes the auth authority after validation. rlm must be the185// caller's own captured cur (asserted via rlm.IsCurrent()); the186// principal is rlm.Previous().Address(). Closes the address-parameter187// forgery: an external realm cannot supply Owner() as `caller` to188// bypass the underlying Authority's check.189//190// SECURITY (runtime substitution): once the current authority approves a191// Transfer, the new authority is installed and effective on the next call.192// If an attacker ever becomes the authority — even briefly — they can193// install a permanent DroppedAuthority (DoS) or an AutoAcceptAuthority194// (privilege escalation). Consumers concerned about this should wrap195// Transfer with a one-shot guard or a quorum/cooldown check.196//197// `newAuthority` is also an open-interface input — see NewWithAuthority's198// Class-3 caveat. Pass canonical impls.199func (a *Authorizer) Transfer(_ int, rlm realm, newAuthority Authority) error {200	if !rlm.IsCurrent() {201		return errors.New("unauthorized")202	}203	caller := rlm.Previous().Address()204	return a.auth.Authorize(caller, "transfer_authority", func() error {205		a.auth = newAuthority206		return nil207	})208}209210// DoByCurrent executes a privileged action authorized as `rlm`. `rlm`211// must be the caller's own live cur (asserted via rlm.IsCurrent());212// the authorized principal is `rlm.Address()`. To authorize as the213// realm that called your function, use `DoByPrevious`.214//215//	auth.DoByCurrent(0, cur, "update_config", func() error { ... })    // current realm authorizes216//	auth.DoByPrevious(0, cur, "update_config", func() error { ... })   // calling realm authorizes217//218// The `_ int` first parameter is a deliberate sentinel that pushes219// `rlm realm` past the first-arg position so DoByCurrent stays a220// non-crossing method — otherwise it would be a crossing method and221// rlm.Previous() inside would resolve one realm deeper than the caller222// intended.223//224// SECURITY: the IsCurrent guard closes Class-2 designation forgery (see225// docs/resources/gno-security.md). A realm value's .Address() is set226// when the value is minted at a crossing frame; the value can in227// principle be stored and replayed. Without IsCurrent, a hostile realm228// could capture a high-privilege realm's cur.Previous() (e.g., when229// that realm called into it) and later pass the stored value here to230// authorize actions as that realm. IsCurrent rejects stale captures by231// requiring the value to match the topmost live crossing frame's cur.232func (a *Authorizer) DoByCurrent(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {233	if !rlm.IsCurrent() {234		return errors.New("unauthorized")235	}236	return a.auth.Authorize(rlm.Address(), title, action, args...)237}238239// DoByPrevious executes a privileged action authorized as the realm240// that called the function invoking DoByPrevious. `rlm` must be the241// caller's own live cur; the principal is derived as242// `rlm.Previous().Address()`. Mirrors the Transfer/AddMember pattern:243// always take live cur, derive the caller-of-caller internally rather244// than accepting a stored/forwarded realm value.245func (a *Authorizer) DoByPrevious(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {246	if !rlm.IsCurrent() {247		return errors.New("unauthorized")248	}249	return a.auth.Authorize(rlm.Previous().Address(), title, action, args...)250}251252// String returns a string representation of the auth authority.253//254// A non-canonical impl is wrapped as custom_authority[...] so that the255// official "dropped" is distinguishable from a "*custom*: dropped"256// (autoclaimed) one. Shared with ContractAuthority.String, which applies257// the same rule to its nested proposer.258func (a *Authorizer) String() string {259	return canonicalAuthorityString(a.auth)260}261262// MemberAuthority is the default implementation using addrset for member263// management.264type MemberAuthority struct {265	members addrset.Set266}267268func NewMemberAuthority(members ...address) *MemberAuthority {269	auth := &MemberAuthority{}270	for _, addr := range members {271		auth.members.Add(addr)272	}273	return auth274}275276func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {277	if !a.members.Has(caller) {278		return errors.New("unauthorized")279	}280281	if err := action(); err != nil {282		return err283	}284	return nil285}286287func (a *MemberAuthority) String() string {288	addrs := []string{}289	a.members.IterateByOffset(0, a.members.Size(), func(addr address) bool {290		addrs = append(addrs, string(addr))291		return false292	})293	addrsStr := strings.Join(addrs, ",")294	return ufmt.Sprintf("member_authority[%s]", addrsStr)295}296297// AddMember adds a new member to the authority. rlm must be the caller's298// own captured cur; the principal is rlm.Previous().Address() and must299// already be a member. The IsCurrent guard closes the forgery where an300// external realm passes Owner() as caller to bypass members.Has(caller).301func (a *MemberAuthority) AddMember(_ int, rlm realm, addr address) error {302	if !rlm.IsCurrent() {303		return errors.New("unauthorized")304	}305	caller := rlm.Previous().Address()306	return a.Authorize(caller, "add_member", func() error {307		a.members.Add(addr)308		return nil309	})310}311312// AddMembers adds a list of members to the authority. Same rlm contract313// as AddMember.314func (a *MemberAuthority) AddMembers(_ int, rlm realm, addrs ...address) error {315	if !rlm.IsCurrent() {316		return errors.New("unauthorized")317	}318	caller := rlm.Previous().Address()319	return a.Authorize(caller, "add_members", func() error {320		for _, addr := range addrs {321			a.members.Add(addr)322		}323		return nil324	})325}326327// RemoveMember removes a member from the authority. Same rlm contract328// as AddMember.329func (a *MemberAuthority) RemoveMember(_ int, rlm realm, addr address) error {330	if !rlm.IsCurrent() {331		return errors.New("unauthorized")332	}333	caller := rlm.Previous().Address()334	return a.Authorize(caller, "remove_member", func() error {335		a.members.Remove(addr)336		return nil337	})338}339340// Has checks if the given address is a member of the authority341func (a *MemberAuthority) Has(addr address) bool {342	return a.members.Has(addr)343}344345// ContractAuthority implements async contract-based authority346type ContractAuthority struct {347	contractPath    string348	contractAddr    address349	contractHandler PrivilegedActionHandler350	proposer        Authority // controls who can create proposals351}352353// NewContractAuthority makes the contract at `path` its OWN authority:354// the default proposer accepts only chain.PackageAddress(path), so a355// privileged action proceeds only when driven from that contract's own356// frame.357//358// NewRestrictedContractAuthority does NOT widen this — it REPLACES it.359// See its godoc; with an explicit proposer, `path` is a label and360// contractAddr is never consulted.361//362// The `caller` compared against that address is established upstream by363// Authorizer.DoByCurrent / DoByPrevious / Transfer under rlm.IsCurrent(),364// so an external realm cannot present the contract address.365//366// BREAKING: the default proposer used to be367// NewAutoAcceptAuthority ("anyone can propose"). Consumers that relied on368// arbitrary callers driving a plain ContractAuthority will start getting369// "unauthorized"; that is the fix, not a regression. Pass an explicit370// proposer to NewRestrictedContractAuthority to opt back in.371//372// SECURITY — the gate binds the contract's IDENTITY, not its intent.373// Inside any crossing frame of the contract, rlm.Address() is374// unconditionally PackageAddress(path), so `DoByCurrent` from within the375// contract is a tautology; only DoByPrevious and Transfer gain a real376// check. Consequently ANY exported function of the contract that returns377// a crossing closure (or anything else carrying its frame) hands this378// authority to its caller. Keep privileged closures unexported.379//380// So DO NOT write NewContractAuthority(ownPath) + DoByCurrent. It reads381// like "only I may do this" and compiles to "anyone may do this". Point382// `path` at the principal you actually want to assert — usually the383// governance realm that will drive the action — and use DoByPrevious, so384// the comparison is against whoever crossed in:385//386//	// in r/gnops/valopers/init.gno387//	auth = authz.NewWithAuthority(388//	    authz.NewContractAuthority("gno.land/r/gov/dao", handler),389//	)390//	// in the privileged write391//	auth.DoByPrevious(0, rlm, "update-instructions", action)392//393// Own-path is right only when something OTHER than the contract's own394// frame supplies the caller — i.e. you drive it via DoByPrevious from a395// realm you have deliberately let call in, or via Transfer.396//397// SECURITY — a gate is only as good as the principal it names. Pointing398// `path` at a realm whose frame ANY caller can mint buys nothing. Check,399// for whatever principal you choose, that nothing exported by that realm400// hands out its frame identity:401//402//   - `gno.land/r/gov/dao` is safe to assert ONLY because403//     SimpleExecutor.Execute rejects invocation from outside the proxy404//     (r/gov/dao/types.gno). Before that gate existed, dao's exported405//     NewSimpleExecutor + Execute let any realm mint a frame whose406//     Previous() was r/gov/dao, so this whole pairing authenticated407//     nothing. Do not assume a governance path is unforgeable; verify it.408//   - Whatever the principal, an exported function of YOUR realm whose409//     signature is assignable to a callback type the principal invokes410//     (for r/gov/dao: `func(realm) error`) is itself the leak — the411//     principal will happily call it for an attacker. Keep privileged412//     entrypoints out of that shape, or take extra parameters.413//414// Neither shape defends against re-exporting the privileged closure: its415// holder picks the Previous() they present by wrapping it in an executor416// of their own. Unexported closures remain the load-bearing rule.417//418// A syntactically valid but WRONG `path` is a permanent brick, not an419// error: the authority accepts nobody, and Transfer routes through the420// same gate, so it cannot be rotated out either. Construction rejects421// malformed paths, but it cannot tell a wrong path from a right one —422// e.g. "gno.land/r/gov/dao/impl/v0" (the path r/gov/dao's own423// allowedDAOs list holds) is well-formed and permanently dead, because424// an executor's Previous() is the PROXY path, never the impl. Give the425// consumer a governance-gated rotation entrypoint before deploying; see426// r/gnops/valopers/admin.gno's NewAuthorityRotationProposalRequest.427//428// SECURITY (Class-4 captured callback): `handler` is a caller-supplied429// closure that runs SYNCHRONOUSLY inside Authorize, with the consumer's430// authority. A hostile handler can swallow actions, log the caller, or431// execute arbitrary code under the consumer's frame. The package-internal432// wrappedAction enforces at-most-once invocation and NOTHING ELSE — in433// particular it does not check the caller, and the handler may call it434// however it likes (multiple times, never, out of order). Register only435// trusted handler functions; treat handler registration as the trust436// boundary.437//438// Panics if `path` is empty or malformed, or if `handler` is nil.439func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {440	addr := chain.PackageAddress(assertValidContractPath(path))441	// The default proposer is a real Authority, not an absent one. Encoding442	// the strictest policy as a nil field would mean the secure default is443	// what you get by leaving something OUT: a struct literal that skips444	// this constructor, a persisted value from an older build, or re-adding445	// `if proposer == nil { proposer = NewAutoAcceptAuthority() }` would all446	// silently be wide open again. A one-field struct costs the same as the447	// nil it replaces and cannot be reached by omission.448	return newContractAuthority(path, addr, handler, &contractIdentityAuthority{addr: addr})449}450451// NewRestrictedContractAuthority creates a contract authority whose452// proposer REPLACES the contract-identity gate.453//454// This is the escape hatch, not a second lock. `proposer` becomes the ONLY455// authorization decision: contractAddr is not consulted on this path, so456// `path` degrades to a label that appears in String() and asserts nothing.457// NewRestrictedContractAuthority(p, h, NewAutoAcceptAuthority()) is exactly458// the old default behaviour ("anyone can propose"), stated459// explicitly rather than reached by default.460//461// In particular NewRestrictedContractAuthority("gno.land/r/gov/dao", h,462// NewMemberAuthority(alice)) does NOT mean "GovDAO and alice". It means463// "alice, and not GovDAO".464//465// SECURITY:466//   - `handler` is the same Class-4 captured-callback risk as467//     NewContractAuthority — runs synchronously inside Authorize with the468//     consumer's authority. Register only trusted handler functions.469//   - `proposer` is an open-interface input (Class-3 impl-substitution).470//     A hostile proposer Authority can always-approve creation of any471//     proposal, defeating the restriction. Pass canonical impls only.472//     String() renders a non-canonical proposer wrapped as473//     custom_authority[...] so such a substitution is at least visible.474func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {475	if proposer == nil {476		panic("proposer cannot be nil")477	}478	addr := chain.PackageAddress(assertValidContractPath(path))479	return newContractAuthority(path, addr, handler, proposer)480}481482// newContractAuthority is the single construction path: both exported483// constructors validate, then land here, so a guard added once applies to484// both. `handler` is rejected here rather than surfaced at Authorize time485// because Authorize checks contractHandler == nil BEFORE consulting the486// proposer, and Transfer routes through Authorize — so a nil-handler487// authority could never be rotated out. It is a permanent brick with no488// on-chain recovery for anyone: the same bricked-governance failure489// mode, reached by accident.490func newContractAuthority(path string, addr address, handler PrivilegedActionHandler, proposer Authority) *ContractAuthority {491	if handler == nil {492		panic("contract handler cannot be nil")493	}494	return &ContractAuthority{495		contractPath:    path,496		contractAddr:    addr,497		contractHandler: handler,498		proposer:        proposer,499	}500}501502// assertValidContractPath rejects paths that chain.PackageAddress would503// hash happily but that no realm could ever present, which would brick the504// authority permanently (nobody authorizes, and Transfer routes through the505// same gate so nobody can rotate out). Returns `path` so it composes.506//507// The authoritative rule is gnolang.ReGnoUserPkgPath ("all paths must be508// lowercase ascii alphanumeric characters", gnovm/pkg/gnolang/mempackage.go),509// but no validator is exported to Gno code — chain's isValidSubpath /510// assertValidSubpath are unexported, and there is no chain.IsValidPkgPath.511// So this is a deliberately permissive superset: segment ("/" segment)*,512// segment = [a-z0-9] ([a-z0-9_.-]* [a-z0-9])?. It accepts every real513// package path and catches what actually gets typed wrong — trailing or514// embedded whitespace, empty segments, uppercase, stray punctuation. It515// CANNOT catch a well-formed path that is simply the wrong principal. See516// NewContractAuthority's godoc on rotation.517func assertValidContractPath(path string) string {518	if path == "" {519		panic("contract path cannot be empty")520	}521	if !strings.Contains(path, "/") {522		panic("contract path must be a package path, e.g. gno.land/r/gov/dao")523	}524	start := 0525	for i := 0; i <= len(path); i++ {526		if i == len(path) || path[i] == '/' {527			if !isValidContractPathSegment(path[start:i]) {528				panic("contract path must be '/'-separated segments of [a-z0-9] with '_.-' allowed inside a segment, e.g. gno.land/r/gov/dao")529			}530			start = i + 1531		}532	}533	return path534}535536func isValidContractPathSegment(seg string) bool {537	if seg == "" {538		return false539	}540	for i := 0; i < len(seg); i++ {541		switch c := seg[i]; {542		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':543		case c == '_' || c == '.' || c == '-':544			// Allowed only strictly inside a segment.545			if i == 0 || i == len(seg)-1 {546				return false547			}548		default:549			return false550		}551	}552	return true553}554555// contractIdentityAuthority is the default proposer installed by556// NewContractAuthority: the contract at the bound path, and nobody else.557// Unexported and unmutable by design — AddMember/RemoveMember are558// meaningless for a fixed contract identity, which is why this is not a559// MemberAuthority holding one address.560type contractIdentityAuthority struct {561	addr address562}563564func (a *contractIdentityAuthority) Authorize(caller address, _ string, action PrivilegedAction, _ ...any) error {565	if caller != a.addr {566		return errors.New("unauthorized")567	}568	return action()569}570571func (a *contractIdentityAuthority) String() string { return "contract-identity" }572573func (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {574	if a.contractHandler == nil {575		return errors.New("contract handler is not set")576	}577578	// setup a once instance to ensure the action is executed only once579	executionOnce := once.Once{}580581	// wrappedAction enforces at-most-once invocation. The previous582	// gate `unsafe.CurrentRealm() == contractAddr` is removed: it583	// was .Title()-bypassable (runtime.CurrentRealm walks past584	// non-crossing frames to the most-recent crossing ancestor) and585	// the trust boundary is now upstream — Authorizer.DoByCurrent /586	// DoByPrevious require rlm.IsCurrent() and pass a non-forgeable587	// principal to Authorize, while the consumer realm's handler588	// closure is the Class-4 trust root by lexical capture at589	// registration time.590	wrappedAction := func() error {591		return executionOnce.DoErr(func() error {592			return action()593		})594	}595596	handle := func() error {597		if err := a.contractHandler(title, wrappedAction); err != nil {598			return err599		}600		return nil601	}602603	// The proposer IS the authorization decision. For an authority built by604	// NewContractAuthority it is a contractIdentityAuthority bound to605	// contractAddr — the contract itself and nobody else; for one built by606	// NewRestrictedContractAuthority it is whatever the consumer installed,607	// and contractAddr is deliberately not consulted (see that constructor's608	// godoc). `caller` is established upstream by Authorizer.DoByCurrent /609	// DoByPrevious / Transfer under rlm.IsCurrent(), so an external realm610	// cannot present an arbitrary principal here.611	//612	// A nil proposer is not a policy, it is a malformed value: both613	// constructors always install one, so nil means the struct was built by614	// a literal that bypassed them. Fail closed rather than dereference.615	if a.proposer == nil {616		return errors.New("proposer is not set")617	}618	return a.proposer.Authorize(caller, title+"_proposal", handle, args...)619}620621// String renders the contract path AND the proposer.622//623// The proposer half is security-relevant, not cosmetic: it is the only624// thing distinguishing a gated authority from a wide-open one. Rendering625// the path alone made626//627//	NewContractAuthority(path, handler)                              // gated628//	NewRestrictedContractAuthority(path, handler, AutoAccept{})      // open to all629//630// byte-identical, so any consumer test asserting on this string — and631// any on-chain reader inspecting it — was blind to the difference. A632// consumer realm could have its authority swapped for a fully permissive633// one and its assertions would stay green.634//635// "contract-identity" names the default installed by NewContractAuthority:636// the contract at contractPath, and nobody else, may drive this authority.637//638// The proposer is rendered through canonicalAuthorityString, NOT by calling639// a.proposer.String() directly. Authority is an open interface, so a foreign640// impl can return any text it likes — including "contract-identity". That641// made a fully permissive authority byte-identical to the gated default642// again, defeating the very assertions this rendering exists to support.643// Non-canonical impls are wrapped as custom_authority[...], mirroring what644// Authorizer.String has always done at the outer level.645//646// Read the `contract=` half with care: it is load-bearing only for the647// contract-identity default. With any other proposer it is a label —648// see NewRestrictedContractAuthority.649func (a *ContractAuthority) String() string {650	return ufmt.Sprintf(651		"contract_authority[contract=%s,proposer=%s]",652		a.contractPath,653		canonicalAuthorityString(a.proposer),654	)655}656657// canonicalAuthorityString renders an Authority, wrapping any658// implementation that is not one of this package's own as659// custom_authority[...] so a foreign impl cannot impersonate a canonical660// one by choosing its String() text. Shared by ContractAuthority.String661// (for the nested proposer) and Authorizer.String (for the installed662// authority).663func canonicalAuthorityString(auth Authority) string {664	if auth == nil {665		// Only reachable via a struct literal that bypassed the666		// constructors; Authorize fails closed on the same condition.667		return "<unset>"668	}669	switch auth.(type) {670	case *MemberAuthority, *ContractAuthority, *AutoAcceptAuthority,671		*droppedAuthority, *contractIdentityAuthority:672		return auth.String()673	default:674		return ufmt.Sprintf("custom_authority[%s]", auth.String())675	}676}677678// AutoAcceptAuthority implements an authority that accepts all actions679// AutoAcceptAuthority is a simple authority that automatically accepts all680// actions.681// It can be used as a proposer authority to allow anyone to create proposals.682type AutoAcceptAuthority struct{}683684func NewAutoAcceptAuthority() *AutoAcceptAuthority {685	return &AutoAcceptAuthority{}686}687688func (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {689	return action()690}691692func (a *AutoAcceptAuthority) String() string {693	return "auto_accept_authority"694}695696// droppedAuthority implements an authority that denies all actions697type droppedAuthority struct{}698699func NewDroppedAuthority() Authority {700	return &droppedAuthority{}701}702703func (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {704	return errors.New("dropped authority: all actions are denied")705}706707func (a *droppedAuthority) String() string {708	return "dropped_authority"709}710

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.