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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

authz.gno

28.79 Kb · 697 lines
  1// Package authz provides flexible authorization control for privileged actions.
  2//
  3// # Authorization Strategies
  4//
  5// The package supports multiple authorization strategies:
  6//   - Member-based: Single user or team of users
  7//   - Contract-based: The contract at a given path is itself the authority
  8//   - Auto-accept: Allow all actions
  9//   - Drop: Deny all actions
 10//
 11// Core Components
 12//
 13//   - Authority interface: Base interface implemented by all authorities
 14//   - Authorizer: Main wrapper object for authority management
 15//   - MemberAuthority: Manages authorized addresses
 16//   - ContractAuthority: Makes the contract at a path its own authority
 17//   - AutoAcceptAuthority: Accepts all actions
 18//   - DroppedAuthority: Denies all actions
 19//
 20// Quick Start
 21//
 22//	// Initialize with contract deployer as authority
 23//	var member address(...)
 24//	var auth = authz.NewWithMembers(member)
 25//
 26//	// Create functions that require authorization
 27//	func UpdateConfig(cur realm, newValue string) error {
 28//		return auth.DoByPrevious(0, cur, "update_config", func() error {
 29//			config = newValue
 30//			return nil
 31//		})
 32//	}
 33//
 34// See example_test.gno for more usage examples.
 35package authz
 36
 37import (
 38	"chain"
 39	"errors"
 40	"strings"
 41
 42	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/addrset/v0"
 43	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/once/v0"
 44	"gno.land/p/nt/avl/rotree/v0"
 45	"gno.land/p/nt/avl/v0"
 46	"gno.land/p/nt/ufmt/v0"
 47)
 48
 49// Authorizer is the main wrapper object that handles authority management.
 50// It is configured with a replaceable Authority implementation.
 51type Authorizer struct {
 52	auth Authority
 53}
 54
 55// Authority represents an entity that can authorize privileged actions.
 56// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,
 57// and DroppedAuthority.
 58//
 59// Authority is the canonical safe shape for cross-package authority
 60// interfaces: methods are address-typed (no realm/cur crosses the interface
 61// boundary), and consumers correctly derive `caller` from
 62// `cur.Previous().Address()` under `rlm.IsCurrent()` before invoking
 63// Authorize. No cur-leak (class 1) is possible through this interface.
 64//
 65// However, two RESIDUAL RISKS apply:
 66//
 67//   - Class-3 impl-substitution: NewWithAuthority and Authorizer.Transfer
 68//     accept any Authority impl. A malicious Authority can always-approve
 69//     (silent privilege escalation) or always-deny (denial-of-service).
 70//     Consumers should pass canonical impls from this package
 71//     (MemberAuthority, ContractAuthority, AutoAcceptAuthority,
 72//     DroppedAuthority) unless they have explicit reason to register a
 73//     foreign impl. We do not expose an IsCanonicalAuthority allowlist
 74//     because the package is intentionally extensible — third-party impls
 75//     are the design intent.
 76//
 77//   - Class-4 closed-over-authority: NewContractAuthority and
 78//     NewRestrictedContractAuthority capture a caller-supplied
 79//     PrivilegedActionHandler closure. The handler runs synchronously
 80//     inside Authorize with the consumer's authority. A hostile handler
 81//     can swallow actions, log the caller, or execute arbitrary code
 82//     under the consumer's frame. Register only trusted handler functions.
 83//     See r/gnops/valopers/init.gno for the realistic registration shape.
 84//
 85//   - Caller- and title-forgery on the RAW interface: Authorize takes
 86//     `caller` and `title` as ARGUMENTS, not from the frame. A consumer
 87//     that exposes a bare Authority (rather than the *Authorizer that
 88//     wraps it) therefore lets any holder present any caller and any
 89//     title. Caller-forgery is contained — the only authority-mutating
 90//     closure lives inside Authorizer.Transfer, so a forged caller on a
 91//     raw Authorize runs the caller's own inert closure and cannot
 92//     transfer; TestForgedCallerCannotTransfer pins that boundary.
 93//     Title-forgery is NOT contained: the title is passed straight to the
 94//     contractHandler, so a handler that branches on it (routing, quotas,
 95//     audit trails) must not treat it as trusted. Keep the Authority
 96//     unexported and hand out only what callers need; see
 97//     r/gnops/valopers/admin.gno, which exports a description string.
 98//
 99// We do NOT seal Authority via an unexported marker method — that pattern
100// is bypassable via embedding in Gno; see
101// p/test/seal/filetests/z_seal_*_filetest.gno for the four bypass tests.
102type Authority interface {
103	// Authorize executes a privileged action if the caller is authorized
104	// Additional args can be provided for context (e.g., for proposal creation)
105	Authorize(caller address, title string, action PrivilegedAction, args ...any) error
106
107	// String returns a human-readable description of the authority
108	String() string
109}
110
111// PrivilegedAction defines a function that performs a privileged action.
112type PrivilegedAction func() error
113
114// PrivilegedActionHandler is called by contract-based authorities to handle
115// privileged actions.
116type PrivilegedActionHandler func(title string, action PrivilegedAction) error
117
118// NewWithMembers creates a new Authorizer whose authority is a
119// MemberAuthority containing the given addresses. Callers express
120// authority intent at the call site:
121//
122//	// "auth realm is the authority"
123//	a := authz.NewWithMembers(cur.Address())
124//
125//	// "previous realm is the authority" (from a crossing function)
126//	a := authz.NewWithMembers(cur.Previous().Address())
127//
128//	// "EOA caller is the authority" (from init(cur realm))
129//	if !cur.Previous().IsUserCall() {
130//	    panic("realm must be initialized by EOA")
131//	}
132//	a := authz.NewWithMembers(cur.Previous().Address())
133//
134// This replaces the previous NewWithCurrent / NewWithPrevious /
135// NewWithOrigin sugar — those baked runtime.{Current,Previous,Origin}
136// reads into the constructor, which (a) prevented use from package-
137// level var initializers, (b) made the EOA-origin check inside
138// NewWithOrigin an indirect address comparison rather than the
139// straightforward IsUserCall predicate, and (c) coupled the
140// constructor to the runtime walks the rest of the migration is
141// moving away from.
142func NewWithMembers(addrs ...address) *Authorizer {
143	return &Authorizer{
144		auth: NewMemberAuthority(addrs...),
145	}
146}
147
148// NewWithAuthority creates a new Authorizer with a specific authority.
149//
150// SECURITY: `authority` is an open-interface input — any value satisfying
151// Authority is accepted. A malicious impl can always-approve (privilege
152// escalation) or always-deny (DoS). Prefer canonical impls from this
153// package (NewMemberAuthority, NewContractAuthority, NewAutoAcceptAuthority,
154// NewDroppedAuthority) unless you specifically need a foreign impl.
155func NewWithAuthority(authority Authority) *Authorizer {
156	return &Authorizer{
157		auth: authority,
158	}
159}
160
161// Authority returns the auth authority implementation
162func (a *Authorizer) Authority() Authority {
163	return a.auth
164}
165
166// Transfer changes the auth authority after validation. rlm must be the
167// caller's own captured cur (asserted via rlm.IsCurrent()); the
168// principal is rlm.Previous().Address(). Closes the address-parameter
169// forgery: an external realm cannot supply Owner() as `caller` to
170// bypass the underlying Authority's check.
171//
172// SECURITY (runtime substitution): once the current authority approves a
173// Transfer, the new authority is installed and effective on the next call.
174// If an attacker ever becomes the authority — even briefly — they can
175// install a permanent DroppedAuthority (DoS) or an AutoAcceptAuthority
176// (privilege escalation). Consumers concerned about this should wrap
177// Transfer with a one-shot guard or a quorum/cooldown check.
178//
179// `newAuthority` is also an open-interface input — see NewWithAuthority's
180// Class-3 caveat. Pass canonical impls.
181func (a *Authorizer) Transfer(_ int, rlm realm, newAuthority Authority) error {
182	if !rlm.IsCurrent() {
183		return errors.New("unauthorized")
184	}
185	caller := rlm.Previous().Address()
186	return a.auth.Authorize(caller, "transfer_authority", func() error {
187		a.auth = newAuthority
188		return nil
189	})
190}
191
192// DoByCurrent executes a privileged action authorized as `rlm`. `rlm`
193// must be the caller's own live cur (asserted via rlm.IsCurrent());
194// the authorized principal is `rlm.Address()`. To authorize as the
195// realm that called your function, use `DoByPrevious`.
196//
197//	auth.DoByCurrent(0, cur, "update_config", func() error { ... })    // current realm authorizes
198//	auth.DoByPrevious(0, cur, "update_config", func() error { ... })   // calling realm authorizes
199//
200// The `_ int` first parameter is a deliberate sentinel that pushes
201// `rlm realm` past the first-arg position so DoByCurrent stays a
202// non-crossing method — otherwise it would be a crossing method and
203// rlm.Previous() inside would resolve one realm deeper than the caller
204// intended.
205//
206// SECURITY: the IsCurrent guard closes Class-2 designation forgery (see
207// docs/resources/gno-security.md). A realm value's .Address() is set
208// when the value is minted at a crossing frame; the value can in
209// principle be stored and replayed. Without IsCurrent, a hostile realm
210// could capture a high-privilege realm's cur.Previous() (e.g., when
211// that realm called into it) and later pass the stored value here to
212// authorize actions as that realm. IsCurrent rejects stale captures by
213// requiring the value to match the topmost live crossing frame's cur.
214func (a *Authorizer) DoByCurrent(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {
215	if !rlm.IsCurrent() {
216		return errors.New("unauthorized")
217	}
218	return a.auth.Authorize(rlm.Address(), title, action, args...)
219}
220
221// DoByPrevious executes a privileged action authorized as the realm
222// that called the function invoking DoByPrevious. `rlm` must be the
223// caller's own live cur; the principal is derived as
224// `rlm.Previous().Address()`. Mirrors the Transfer/AddMember pattern:
225// always take live cur, derive the caller-of-caller internally rather
226// than accepting a stored/forwarded realm value.
227func (a *Authorizer) DoByPrevious(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {
228	if !rlm.IsCurrent() {
229		return errors.New("unauthorized")
230	}
231	return a.auth.Authorize(rlm.Previous().Address(), title, action, args...)
232}
233
234// String returns a string representation of the auth authority.
235//
236// A non-canonical impl is wrapped as custom_authority[...] so that the
237// official "dropped" is distinguishable from a "*custom*: dropped"
238// (autoclaimed) one. Shared with ContractAuthority.String, which applies
239// the same rule to its nested proposer.
240func (a *Authorizer) String() string {
241	return canonicalAuthorityString(a.auth)
242}
243
244// MemberAuthority is the default implementation using addrset for member
245// management.
246type MemberAuthority struct {
247	members addrset.Set
248}
249
250func NewMemberAuthority(members ...address) *MemberAuthority {
251	auth := &MemberAuthority{}
252	for _, addr := range members {
253		auth.members.Add(addr)
254	}
255	return auth
256}
257
258func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
259	if !a.members.Has(caller) {
260		return errors.New("unauthorized")
261	}
262
263	if err := action(); err != nil {
264		return err
265	}
266	return nil
267}
268
269func (a *MemberAuthority) String() string {
270	addrs := []string{}
271	a.members.Tree().Iterate("", "", func(key string, _ any) bool {
272		addrs = append(addrs, key)
273		return false
274	})
275	addrsStr := strings.Join(addrs, ",")
276	return ufmt.Sprintf("member_authority[%s]", addrsStr)
277}
278
279// AddMember adds a new member to the authority. rlm must be the caller's
280// own captured cur; the principal is rlm.Previous().Address() and must
281// already be a member. The IsCurrent guard closes the forgery where an
282// external realm passes Owner() as caller to bypass members.Has(caller).
283func (a *MemberAuthority) AddMember(_ int, rlm realm, addr address) error {
284	if !rlm.IsCurrent() {
285		return errors.New("unauthorized")
286	}
287	caller := rlm.Previous().Address()
288	return a.Authorize(caller, "add_member", func() error {
289		a.members.Add(addr)
290		return nil
291	})
292}
293
294// AddMembers adds a list of members to the authority. Same rlm contract
295// as AddMember.
296func (a *MemberAuthority) AddMembers(_ int, rlm realm, addrs ...address) error {
297	if !rlm.IsCurrent() {
298		return errors.New("unauthorized")
299	}
300	caller := rlm.Previous().Address()
301	return a.Authorize(caller, "add_members", func() error {
302		for _, addr := range addrs {
303			a.members.Add(addr)
304		}
305		return nil
306	})
307}
308
309// RemoveMember removes a member from the authority. Same rlm contract
310// as AddMember.
311func (a *MemberAuthority) RemoveMember(_ int, rlm realm, addr address) error {
312	if !rlm.IsCurrent() {
313		return errors.New("unauthorized")
314	}
315	caller := rlm.Previous().Address()
316	return a.Authorize(caller, "remove_member", func() error {
317		a.members.Remove(addr)
318		return nil
319	})
320}
321
322// Tree returns a read-only view of the members tree
323func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree {
324	tree := a.members.Tree().(*avl.Tree)
325	return rotree.Wrap(tree, nil)
326}
327
328// Has checks if the given address is a member of the authority
329func (a *MemberAuthority) Has(addr address) bool {
330	return a.members.Has(addr)
331}
332
333// ContractAuthority implements async contract-based authority
334type ContractAuthority struct {
335	contractPath    string
336	contractAddr    address
337	contractHandler PrivilegedActionHandler
338	proposer        Authority // controls who can create proposals
339}
340
341// NewContractAuthority makes the contract at `path` its OWN authority:
342// the default proposer accepts only chain.PackageAddress(path), so a
343// privileged action proceeds only when driven from that contract's own
344// frame.
345//
346// NewRestrictedContractAuthority does NOT widen this — it REPLACES it.
347// See its godoc; with an explicit proposer, `path` is a label and
348// contractAddr is never consulted.
349//
350// The `caller` compared against that address is established upstream by
351// Authorizer.DoByCurrent / DoByPrevious / Transfer under rlm.IsCurrent(),
352// so an external realm cannot present the contract address.
353//
354// BREAKING: the default proposer used to be
355// NewAutoAcceptAuthority ("anyone can propose"). Consumers that relied on
356// arbitrary callers driving a plain ContractAuthority will start getting
357// "unauthorized"; that is the fix, not a regression. Pass an explicit
358// proposer to NewRestrictedContractAuthority to opt back in.
359//
360// SECURITY — the gate binds the contract's IDENTITY, not its intent.
361// Inside any crossing frame of the contract, rlm.Address() is
362// unconditionally PackageAddress(path), so `DoByCurrent` from within the
363// contract is a tautology; only DoByPrevious and Transfer gain a real
364// check. Consequently ANY exported function of the contract that returns
365// a crossing closure (or anything else carrying its frame) hands this
366// authority to its caller. Keep privileged closures unexported.
367//
368// So DO NOT write NewContractAuthority(ownPath) + DoByCurrent. It reads
369// like "only I may do this" and compiles to "anyone may do this". Point
370// `path` at the principal you actually want to assert — usually the
371// governance realm that will drive the action — and use DoByPrevious, so
372// the comparison is against whoever crossed in:
373//
374//	// in r/gnops/valopers/init.gno
375//	auth = authz.NewWithAuthority(
376//	    authz.NewContractAuthority("gno.land/r/gov/dao", handler),
377//	)
378//	// in the privileged write
379//	auth.DoByPrevious(0, rlm, "update-instructions", action)
380//
381// Own-path is right only when something OTHER than the contract's own
382// frame supplies the caller — i.e. you drive it via DoByPrevious from a
383// realm you have deliberately let call in, or via Transfer.
384//
385// SECURITY — a gate is only as good as the principal it names. Pointing
386// `path` at a realm whose frame ANY caller can mint buys nothing. Check,
387// for whatever principal you choose, that nothing exported by that realm
388// hands out its frame identity:
389//
390//   - `gno.land/r/gov/dao` is safe to assert ONLY because
391//     SimpleExecutor.Execute rejects invocation from outside the proxy
392//     (r/gov/dao/types.gno). Before that gate existed, dao's exported
393//     NewSimpleExecutor + Execute let any realm mint a frame whose
394//     Previous() was r/gov/dao, so this whole pairing authenticated
395//     nothing. Do not assume a governance path is unforgeable; verify it.
396//   - Whatever the principal, an exported function of YOUR realm whose
397//     signature is assignable to a callback type the principal invokes
398//     (for r/gov/dao: `func(realm) error`) is itself the leak — the
399//     principal will happily call it for an attacker. Keep privileged
400//     entrypoints out of that shape, or take extra parameters.
401//
402// Neither shape defends against re-exporting the privileged closure: its
403// holder picks the Previous() they present by wrapping it in an executor
404// of their own. Unexported closures remain the load-bearing rule.
405//
406// A syntactically valid but WRONG `path` is a permanent brick, not an
407// error: the authority accepts nobody, and Transfer routes through the
408// same gate, so it cannot be rotated out either. Construction rejects
409// malformed paths, but it cannot tell a wrong path from a right one —
410// e.g. "gno.land/r/gov/dao/impl/v0" (the path r/gov/dao's own
411// allowedDAOs list holds) is well-formed and permanently dead, because
412// an executor's Previous() is the PROXY path, never the impl. Give the
413// consumer a governance-gated rotation entrypoint before deploying; see
414// r/gnops/valopers/admin.gno's NewAuthorityRotationProposalRequest.
415//
416// SECURITY (Class-4 captured callback): `handler` is a caller-supplied
417// closure that runs SYNCHRONOUSLY inside Authorize, with the consumer's
418// authority. A hostile handler can swallow actions, log the caller, or
419// execute arbitrary code under the consumer's frame. The package-internal
420// wrappedAction enforces at-most-once invocation and NOTHING ELSE — in
421// particular it does not check the caller, and the handler may call it
422// however it likes (multiple times, never, out of order). Register only
423// trusted handler functions; treat handler registration as the trust
424// boundary.
425//
426// Panics if `path` is empty or malformed, or if `handler` is nil.
427func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {
428	addr := chain.PackageAddress(assertValidContractPath(path))
429	// The default proposer is a real Authority, not an absent one. Encoding
430	// the strictest policy as a nil field would mean the secure default is
431	// what you get by leaving something OUT: a struct literal that skips
432	// this constructor, a persisted value from an older build, or re-adding
433	// `if proposer == nil { proposer = NewAutoAcceptAuthority() }` would all
434	// silently be wide open again. A one-field struct costs the same as the
435	// nil it replaces and cannot be reached by omission.
436	return newContractAuthority(path, addr, handler, &contractIdentityAuthority{addr: addr})
437}
438
439// NewRestrictedContractAuthority creates a contract authority whose
440// proposer REPLACES the contract-identity gate.
441//
442// This is the escape hatch, not a second lock. `proposer` becomes the ONLY
443// authorization decision: contractAddr is not consulted on this path, so
444// `path` degrades to a label that appears in String() and asserts nothing.
445// NewRestrictedContractAuthority(p, h, NewAutoAcceptAuthority()) is exactly
446// the old default behaviour ("anyone can propose"), stated
447// explicitly rather than reached by default.
448//
449// In particular NewRestrictedContractAuthority("gno.land/r/gov/dao", h,
450// NewMemberAuthority(alice)) does NOT mean "GovDAO and alice". It means
451// "alice, and not GovDAO".
452//
453// SECURITY:
454//   - `handler` is the same Class-4 captured-callback risk as
455//     NewContractAuthority — runs synchronously inside Authorize with the
456//     consumer's authority. Register only trusted handler functions.
457//   - `proposer` is an open-interface input (Class-3 impl-substitution).
458//     A hostile proposer Authority can always-approve creation of any
459//     proposal, defeating the restriction. Pass canonical impls only.
460//     String() renders a non-canonical proposer wrapped as
461//     custom_authority[...] so such a substitution is at least visible.
462func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {
463	if proposer == nil {
464		panic("proposer cannot be nil")
465	}
466	addr := chain.PackageAddress(assertValidContractPath(path))
467	return newContractAuthority(path, addr, handler, proposer)
468}
469
470// newContractAuthority is the single construction path: both exported
471// constructors validate, then land here, so a guard added once applies to
472// both. `handler` is rejected here rather than surfaced at Authorize time
473// because Authorize checks contractHandler == nil BEFORE consulting the
474// proposer, and Transfer routes through Authorize — so a nil-handler
475// authority could never be rotated out. It is a permanent brick with no
476// on-chain recovery for anyone: the same bricked-governance failure
477// mode, reached by accident.
478func newContractAuthority(path string, addr address, handler PrivilegedActionHandler, proposer Authority) *ContractAuthority {
479	if handler == nil {
480		panic("contract handler cannot be nil")
481	}
482	return &ContractAuthority{
483		contractPath:    path,
484		contractAddr:    addr,
485		contractHandler: handler,
486		proposer:        proposer,
487	}
488}
489
490// assertValidContractPath rejects paths that chain.PackageAddress would
491// hash happily but that no realm could ever present, which would brick the
492// authority permanently (nobody authorizes, and Transfer routes through the
493// same gate so nobody can rotate out). Returns `path` so it composes.
494//
495// The authoritative rule is gnolang.ReGnoUserPkgPath ("all paths must be
496// lowercase ascii alphanumeric characters", gnovm/pkg/gnolang/mempackage.go),
497// but no validator is exported to Gno code — chain's isValidSubpath /
498// assertValidSubpath are unexported, and there is no chain.IsValidPkgPath.
499// So this is a deliberately permissive superset: segment ("/" segment)*,
500// segment = [a-z0-9] ([a-z0-9_.-]* [a-z0-9])?. It accepts every real
501// package path and catches what actually gets typed wrong — trailing or
502// embedded whitespace, empty segments, uppercase, stray punctuation. It
503// CANNOT catch a well-formed path that is simply the wrong principal. See
504// NewContractAuthority's godoc on rotation.
505func assertValidContractPath(path string) string {
506	if path == "" {
507		panic("contract path cannot be empty")
508	}
509	if !strings.Contains(path, "/") {
510		panic("contract path must be a package path, e.g. gno.land/r/gov/dao")
511	}
512	start := 0
513	for i := 0; i <= len(path); i++ {
514		if i == len(path) || path[i] == '/' {
515			if !isValidContractPathSegment(path[start:i]) {
516				panic("contract path must be '/'-separated segments of [a-z0-9] with '_.-' allowed inside a segment, e.g. gno.land/r/gov/dao")
517			}
518			start = i + 1
519		}
520	}
521	return path
522}
523
524func isValidContractPathSegment(seg string) bool {
525	if seg == "" {
526		return false
527	}
528	for i := 0; i < len(seg); i++ {
529		switch c := seg[i]; {
530		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
531		case c == '_' || c == '.' || c == '-':
532			// Allowed only strictly inside a segment.
533			if i == 0 || i == len(seg)-1 {
534				return false
535			}
536		default:
537			return false
538		}
539	}
540	return true
541}
542
543// contractIdentityAuthority is the default proposer installed by
544// NewContractAuthority: the contract at the bound path, and nobody else.
545// Unexported and unmutable by design — AddMember/RemoveMember are
546// meaningless for a fixed contract identity, which is why this is not a
547// MemberAuthority holding one address.
548type contractIdentityAuthority struct {
549	addr address
550}
551
552func (a *contractIdentityAuthority) Authorize(caller address, _ string, action PrivilegedAction, _ ...any) error {
553	if caller != a.addr {
554		return errors.New("unauthorized")
555	}
556	return action()
557}
558
559func (a *contractIdentityAuthority) String() string { return "contract-identity" }
560
561func (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
562	if a.contractHandler == nil {
563		return errors.New("contract handler is not set")
564	}
565
566	// setup a once instance to ensure the action is executed only once
567	executionOnce := once.Once{}
568
569	// wrappedAction enforces at-most-once invocation. The previous
570	// gate `unsafe.CurrentRealm() == contractAddr` is removed: it
571	// was .Title()-bypassable (runtime.CurrentRealm walks past
572	// non-crossing frames to the most-recent crossing ancestor) and
573	// the trust boundary is now upstream — Authorizer.DoByCurrent /
574	// DoByPrevious require rlm.IsCurrent() and pass a non-forgeable
575	// principal to Authorize, while the consumer realm's handler
576	// closure is the Class-4 trust root by lexical capture at
577	// registration time.
578	wrappedAction := func() error {
579		return executionOnce.DoErr(func() error {
580			return action()
581		})
582	}
583
584	handle := func() error {
585		if err := a.contractHandler(title, wrappedAction); err != nil {
586			return err
587		}
588		return nil
589	}
590
591	// The proposer IS the authorization decision. For an authority built by
592	// NewContractAuthority it is a contractIdentityAuthority bound to
593	// contractAddr — the contract itself and nobody else; for one built by
594	// NewRestrictedContractAuthority it is whatever the consumer installed,
595	// and contractAddr is deliberately not consulted (see that constructor's
596	// godoc). `caller` is established upstream by Authorizer.DoByCurrent /
597	// DoByPrevious / Transfer under rlm.IsCurrent(), so an external realm
598	// cannot present an arbitrary principal here.
599	//
600	// A nil proposer is not a policy, it is a malformed value: both
601	// constructors always install one, so nil means the struct was built by
602	// a literal that bypassed them. Fail closed rather than dereference.
603	if a.proposer == nil {
604		return errors.New("proposer is not set")
605	}
606	return a.proposer.Authorize(caller, title+"_proposal", handle, args...)
607}
608
609// String renders the contract path AND the proposer.
610//
611// The proposer half is security-relevant, not cosmetic: it is the only
612// thing distinguishing a gated authority from a wide-open one. Rendering
613// the path alone made
614//
615//	NewContractAuthority(path, handler)                              // gated
616//	NewRestrictedContractAuthority(path, handler, AutoAccept{})      // open to all
617//
618// byte-identical, so any consumer test asserting on this string — and
619// any on-chain reader inspecting it — was blind to the difference. A
620// consumer realm could have its authority swapped for a fully permissive
621// one and its assertions would stay green.
622//
623// "contract-identity" names the default installed by NewContractAuthority:
624// the contract at contractPath, and nobody else, may drive this authority.
625//
626// The proposer is rendered through canonicalAuthorityString, NOT by calling
627// a.proposer.String() directly. Authority is an open interface, so a foreign
628// impl can return any text it likes — including "contract-identity". That
629// made a fully permissive authority byte-identical to the gated default
630// again, defeating the very assertions this rendering exists to support.
631// Non-canonical impls are wrapped as custom_authority[...], mirroring what
632// Authorizer.String has always done at the outer level.
633//
634// Read the `contract=` half with care: it is load-bearing only for the
635// contract-identity default. With any other proposer it is a label —
636// see NewRestrictedContractAuthority.
637func (a *ContractAuthority) String() string {
638	return ufmt.Sprintf(
639		"contract_authority[contract=%s,proposer=%s]",
640		a.contractPath,
641		canonicalAuthorityString(a.proposer),
642	)
643}
644
645// canonicalAuthorityString renders an Authority, wrapping any
646// implementation that is not one of this package's own as
647// custom_authority[...] so a foreign impl cannot impersonate a canonical
648// one by choosing its String() text. Shared by ContractAuthority.String
649// (for the nested proposer) and Authorizer.String (for the installed
650// authority).
651func canonicalAuthorityString(auth Authority) string {
652	if auth == nil {
653		// Only reachable via a struct literal that bypassed the
654		// constructors; Authorize fails closed on the same condition.
655		return "<unset>"
656	}
657	switch auth.(type) {
658	case *MemberAuthority, *ContractAuthority, *AutoAcceptAuthority,
659		*droppedAuthority, *contractIdentityAuthority:
660		return auth.String()
661	default:
662		return ufmt.Sprintf("custom_authority[%s]", auth.String())
663	}
664}
665
666// AutoAcceptAuthority implements an authority that accepts all actions
667// AutoAcceptAuthority is a simple authority that automatically accepts all
668// actions.
669// It can be used as a proposer authority to allow anyone to create proposals.
670type AutoAcceptAuthority struct{}
671
672func NewAutoAcceptAuthority() *AutoAcceptAuthority {
673	return &AutoAcceptAuthority{}
674}
675
676func (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
677	return action()
678}
679
680func (a *AutoAcceptAuthority) String() string {
681	return "auto_accept_authority"
682}
683
684// droppedAuthority implements an authority that denies all actions
685type droppedAuthority struct{}
686
687func NewDroppedAuthority() Authority {
688	return &droppedAuthority{}
689}
690
691func (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
692	return errors.New("dropped authority: all actions are denied")
693}
694
695func (a *droppedAuthority) String() string {
696	return "dropped_authority"
697}