authz.gno
29.51 Kb · 709 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/moul/addrset/v1"
43 "gno.land/p/moul/once/v0"
44 "gno.land/p/nt/ufmt/v0"
45)
46
47// Authorizer is the main wrapper object that handles authority management.
48// It is configured with a replaceable Authority implementation.
49type Authorizer struct {
50 auth Authority
51}
52
53// 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 authority
58// interfaces: methods are address-typed (no realm/cur crosses the interface
59// boundary), and consumers correctly derive `caller` from
60// `cur.Previous().Address()` under `rlm.IsCurrent()` before invoking
61// 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.Transfer
66// accept any Authority impl. A malicious Authority can always-approve
67// (silent privilege escalation) or always-deny (denial-of-service).
68// Consumers should pass canonical impls from this package
69// (MemberAuthority, ContractAuthority, AutoAcceptAuthority,
70// DroppedAuthority) unless they have explicit reason to register a
71// foreign impl. We do not expose an IsCanonicalAuthority allowlist
72// because the package is intentionally extensible — third-party impls
73// are the design intent.
74//
75// - Class-4 closed-over-authority: NewContractAuthority and
76// NewRestrictedContractAuthority capture a caller-supplied
77// PrivilegedActionHandler closure. The handler runs synchronously
78// inside Authorize with the consumer's authority. A hostile handler
79// can swallow actions, log the caller, or execute arbitrary code
80// 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 takes
84// `caller` and `title` as ARGUMENTS, not from the frame. A consumer
85// that exposes a bare Authority (rather than the *Authorizer that
86// wraps it) therefore lets any holder present any caller and any
87// title. Caller-forgery is contained — the only authority-mutating
88// closure lives inside Authorizer.Transfer, so a forged caller on a
89// raw Authorize runs the caller's own inert closure and cannot
90// transfer; TestForgedCallerCannotTransfer pins that boundary.
91// Title-forgery is NOT contained: the title is passed straight to the
92// contractHandler, so a handler that branches on it (routing, quotas,
93// audit trails) must not treat it as trusted. Keep the Authority
94// unexported and hand out only what callers need; see
95// r/gnops/valopers/admin.gno, which exports a description string.
96//
97// # Backing store
98//
99// This is the B+ tree successor to [gno.land/p/moul/authz/v0] (which is
100// backed by an AVL tree via gno.land/p/moul/addrset/v0): a bump to v1
101// because the member store — and thus the on-chain storage layout —
102// changed to gno.land/p/moul/addrset/v1, itself backed by
103// gno.land/p/nt/bptree/v0. This is a storage/compatibility change; the
104// exported API is otherwise the same as v0, EXCEPT that the v0
105// MemberAuthority.Tree() escape hatch is intentionally removed so the
106// 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 inside
111// 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 sizes
115// diverge (v0's copies were independent snapshots).
116//
117// We do NOT seal Authority via an unexported marker method — that pattern
118// is bypassable via embedding in Gno; see
119// 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 authorized
122 // Additional args can be provided for context (e.g., for proposal creation)
123 Authorize(caller address, title string, action PrivilegedAction, args ...any) error
124
125 // String returns a human-readable description of the authority
126 String() string
127}
128
129// PrivilegedAction defines a function that performs a privileged action.
130type PrivilegedAction func() error
131
132// PrivilegedActionHandler is called by contract-based authorities to handle
133// privileged actions.
134type PrivilegedActionHandler func(title string, action PrivilegedAction) error
135
136// NewWithMembers creates a new Authorizer whose authority is a
137// MemberAuthority containing the given addresses. Callers express
138// 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 inside
156// NewWithOrigin an indirect address comparison rather than the
157// straightforward IsUserCall predicate, and (c) coupled the
158// constructor to the runtime walks the rest of the migration is
159// moving away from.
160func NewWithMembers(addrs ...address) *Authorizer {
161 return &Authorizer{
162 auth: NewMemberAuthority(addrs...),
163 }
164}
165
166// NewWithAuthority creates a new Authorizer with a specific authority.
167//
168// SECURITY: `authority` is an open-interface input — any value satisfying
169// Authority is accepted. A malicious impl can always-approve (privilege
170// escalation) or always-deny (DoS). Prefer canonical impls from this
171// 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}
178
179// Authority returns the auth authority implementation
180func (a *Authorizer) Authority() Authority {
181 return a.auth
182}
183
184// Transfer changes the auth authority after validation. rlm must be the
185// caller's own captured cur (asserted via rlm.IsCurrent()); the
186// principal is rlm.Previous().Address(). Closes the address-parameter
187// forgery: an external realm cannot supply Owner() as `caller` to
188// bypass the underlying Authority's check.
189//
190// SECURITY (runtime substitution): once the current authority approves a
191// Transfer, the new authority is installed and effective on the next call.
192// If an attacker ever becomes the authority — even briefly — they can
193// install a permanent DroppedAuthority (DoS) or an AutoAcceptAuthority
194// (privilege escalation). Consumers concerned about this should wrap
195// Transfer with a one-shot guard or a quorum/cooldown check.
196//
197// `newAuthority` is also an open-interface input — see NewWithAuthority's
198// 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 = newAuthority
206 return nil
207 })
208}
209
210// 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 the
213// realm that called your function, use `DoByPrevious`.
214//
215// auth.DoByCurrent(0, cur, "update_config", func() error { ... }) // current realm authorizes
216// auth.DoByPrevious(0, cur, "update_config", func() error { ... }) // calling realm authorizes
217//
218// The `_ int` first parameter is a deliberate sentinel that pushes
219// `rlm realm` past the first-arg position so DoByCurrent stays a
220// non-crossing method — otherwise it would be a crossing method and
221// rlm.Previous() inside would resolve one realm deeper than the caller
222// intended.
223//
224// SECURITY: the IsCurrent guard closes Class-2 designation forgery (see
225// docs/resources/gno-security.md). A realm value's .Address() is set
226// when the value is minted at a crossing frame; the value can in
227// principle be stored and replayed. Without IsCurrent, a hostile realm
228// could capture a high-privilege realm's cur.Previous() (e.g., when
229// that realm called into it) and later pass the stored value here to
230// authorize actions as that realm. IsCurrent rejects stale captures by
231// 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}
238
239// DoByPrevious executes a privileged action authorized as the realm
240// that called the function invoking DoByPrevious. `rlm` must be the
241// caller's own live cur; the principal is derived as
242// `rlm.Previous().Address()`. Mirrors the Transfer/AddMember pattern:
243// always take live cur, derive the caller-of-caller internally rather
244// 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}
251
252// String returns a string representation of the auth authority.
253//
254// A non-canonical impl is wrapped as custom_authority[...] so that the
255// official "dropped" is distinguishable from a "*custom*: dropped"
256// (autoclaimed) one. Shared with ContractAuthority.String, which applies
257// the same rule to its nested proposer.
258func (a *Authorizer) String() string {
259 return canonicalAuthorityString(a.auth)
260}
261
262// MemberAuthority is the default implementation using addrset for member
263// management.
264type MemberAuthority struct {
265 members addrset.Set
266}
267
268func NewMemberAuthority(members ...address) *MemberAuthority {
269 auth := &MemberAuthority{}
270 for _, addr := range members {
271 auth.members.Add(addr)
272 }
273 return auth
274}
275
276func (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
277 if !a.members.Has(caller) {
278 return errors.New("unauthorized")
279 }
280
281 if err := action(); err != nil {
282 return err
283 }
284 return nil
285}
286
287func (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 false
292 })
293 addrsStr := strings.Join(addrs, ",")
294 return ufmt.Sprintf("member_authority[%s]", addrsStr)
295}
296
297// AddMember adds a new member to the authority. rlm must be the caller's
298// own captured cur; the principal is rlm.Previous().Address() and must
299// already be a member. The IsCurrent guard closes the forgery where an
300// 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 nil
309 })
310}
311
312// AddMembers adds a list of members to the authority. Same rlm contract
313// 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 nil
324 })
325}
326
327// RemoveMember removes a member from the authority. Same rlm contract
328// 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 nil
337 })
338}
339
340// Has checks if the given address is a member of the authority
341func (a *MemberAuthority) Has(addr address) bool {
342 return a.members.Has(addr)
343}
344
345// ContractAuthority implements async contract-based authority
346type ContractAuthority struct {
347 contractPath string
348 contractAddr address
349 contractHandler PrivilegedActionHandler
350 proposer Authority // controls who can create proposals
351}
352
353// NewContractAuthority makes the contract at `path` its OWN authority:
354// the default proposer accepts only chain.PackageAddress(path), so a
355// privileged action proceeds only when driven from that contract's own
356// frame.
357//
358// NewRestrictedContractAuthority does NOT widen this — it REPLACES it.
359// See its godoc; with an explicit proposer, `path` is a label and
360// contractAddr is never consulted.
361//
362// The `caller` compared against that address is established upstream by
363// 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 be
367// NewAutoAcceptAuthority ("anyone can propose"). Consumers that relied on
368// arbitrary callers driving a plain ContractAuthority will start getting
369// "unauthorized"; that is the fix, not a regression. Pass an explicit
370// 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() is
374// unconditionally PackageAddress(path), so `DoByCurrent` from within the
375// contract is a tautology; only DoByPrevious and Transfer gain a real
376// check. Consequently ANY exported function of the contract that returns
377// a crossing closure (or anything else carrying its frame) hands this
378// authority to its caller. Keep privileged closures unexported.
379//
380// So DO NOT write NewContractAuthority(ownPath) + DoByCurrent. It reads
381// like "only I may do this" and compiles to "anyone may do this". Point
382// `path` at the principal you actually want to assert — usually the
383// governance realm that will drive the action — and use DoByPrevious, so
384// the comparison is against whoever crossed in:
385//
386// // in r/gnops/valopers/init.gno
387// auth = authz.NewWithAuthority(
388// authz.NewContractAuthority("gno.land/r/gov/dao", handler),
389// )
390// // in the privileged write
391// auth.DoByPrevious(0, rlm, "update-instructions", action)
392//
393// Own-path is right only when something OTHER than the contract's own
394// frame supplies the caller — i.e. you drive it via DoByPrevious from a
395// realm you have deliberately let call in, or via Transfer.
396//
397// SECURITY — a gate is only as good as the principal it names. Pointing
398// `path` at a realm whose frame ANY caller can mint buys nothing. Check,
399// for whatever principal you choose, that nothing exported by that realm
400// hands out its frame identity:
401//
402// - `gno.land/r/gov/dao` is safe to assert ONLY because
403// SimpleExecutor.Execute rejects invocation from outside the proxy
404// (r/gov/dao/types.gno). Before that gate existed, dao's exported
405// NewSimpleExecutor + Execute let any realm mint a frame whose
406// Previous() was r/gov/dao, so this whole pairing authenticated
407// nothing. Do not assume a governance path is unforgeable; verify it.
408// - Whatever the principal, an exported function of YOUR realm whose
409// signature is assignable to a callback type the principal invokes
410// (for r/gov/dao: `func(realm) error`) is itself the leak — the
411// principal will happily call it for an attacker. Keep privileged
412// entrypoints out of that shape, or take extra parameters.
413//
414// Neither shape defends against re-exporting the privileged closure: its
415// holder picks the Previous() they present by wrapping it in an executor
416// of their own. Unexported closures remain the load-bearing rule.
417//
418// A syntactically valid but WRONG `path` is a permanent brick, not an
419// error: the authority accepts nobody, and Transfer routes through the
420// same gate, so it cannot be rotated out either. Construction rejects
421// 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 own
423// allowedDAOs list holds) is well-formed and permanently dead, because
424// an executor's Previous() is the PROXY path, never the impl. Give the
425// consumer a governance-gated rotation entrypoint before deploying; see
426// r/gnops/valopers/admin.gno's NewAuthorityRotationProposalRequest.
427//
428// SECURITY (Class-4 captured callback): `handler` is a caller-supplied
429// closure that runs SYNCHRONOUSLY inside Authorize, with the consumer's
430// authority. A hostile handler can swallow actions, log the caller, or
431// execute arbitrary code under the consumer's frame. The package-internal
432// wrappedAction enforces at-most-once invocation and NOTHING ELSE — in
433// particular it does not check the caller, and the handler may call it
434// however it likes (multiple times, never, out of order). Register only
435// trusted handler functions; treat handler registration as the trust
436// 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. Encoding
442 // the strictest policy as a nil field would mean the secure default is
443 // what you get by leaving something OUT: a struct literal that skips
444 // this constructor, a persisted value from an older build, or re-adding
445 // `if proposer == nil { proposer = NewAutoAcceptAuthority() }` would all
446 // silently be wide open again. A one-field struct costs the same as the
447 // nil it replaces and cannot be reached by omission.
448 return newContractAuthority(path, addr, handler, &contractIdentityAuthority{addr: addr})
449}
450
451// NewRestrictedContractAuthority creates a contract authority whose
452// proposer REPLACES the contract-identity gate.
453//
454// This is the escape hatch, not a second lock. `proposer` becomes the ONLY
455// authorization decision: contractAddr is not consulted on this path, so
456// `path` degrades to a label that appears in String() and asserts nothing.
457// NewRestrictedContractAuthority(p, h, NewAutoAcceptAuthority()) is exactly
458// the old default behaviour ("anyone can propose"), stated
459// 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 means
463// "alice, and not GovDAO".
464//
465// SECURITY:
466// - `handler` is the same Class-4 captured-callback risk as
467// NewContractAuthority — runs synchronously inside Authorize with the
468// 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 any
471// proposal, defeating the restriction. Pass canonical impls only.
472// String() renders a non-canonical proposer wrapped as
473// 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}
481
482// newContractAuthority is the single construction path: both exported
483// constructors validate, then land here, so a guard added once applies to
484// both. `handler` is rejected here rather than surfaced at Authorize time
485// because Authorize checks contractHandler == nil BEFORE consulting the
486// proposer, and Transfer routes through Authorize — so a nil-handler
487// authority could never be rotated out. It is a permanent brick with no
488// on-chain recovery for anyone: the same bricked-governance failure
489// 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}
501
502// assertValidContractPath rejects paths that chain.PackageAddress would
503// hash happily but that no realm could ever present, which would brick the
504// authority permanently (nobody authorizes, and Transfer routes through the
505// same gate so nobody can rotate out). Returns `path` so it composes.
506//
507// The authoritative rule is gnolang.ReGnoUserPkgPath ("all paths must be
508// 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 real
513// package path and catches what actually gets typed wrong — trailing or
514// embedded whitespace, empty segments, uppercase, stray punctuation. It
515// CANNOT catch a well-formed path that is simply the wrong principal. See
516// 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 := 0
525 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 + 1
531 }
532 }
533 return path
534}
535
536func isValidContractPathSegment(seg string) bool {
537 if seg == "" {
538 return false
539 }
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 false
547 }
548 default:
549 return false
550 }
551 }
552 return true
553}
554
555// contractIdentityAuthority is the default proposer installed by
556// NewContractAuthority: the contract at the bound path, and nobody else.
557// Unexported and unmutable by design — AddMember/RemoveMember are
558// meaningless for a fixed contract identity, which is why this is not a
559// MemberAuthority holding one address.
560type contractIdentityAuthority struct {
561 addr address
562}
563
564func (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}
570
571func (a *contractIdentityAuthority) String() string { return "contract-identity" }
572
573func (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 }
577
578 // setup a once instance to ensure the action is executed only once
579 executionOnce := once.Once{}
580
581 // wrappedAction enforces at-most-once invocation. The previous
582 // gate `unsafe.CurrentRealm() == contractAddr` is removed: it
583 // was .Title()-bypassable (runtime.CurrentRealm walks past
584 // non-crossing frames to the most-recent crossing ancestor) and
585 // the trust boundary is now upstream — Authorizer.DoByCurrent /
586 // DoByPrevious require rlm.IsCurrent() and pass a non-forgeable
587 // principal to Authorize, while the consumer realm's handler
588 // closure is the Class-4 trust root by lexical capture at
589 // registration time.
590 wrappedAction := func() error {
591 return executionOnce.DoErr(func() error {
592 return action()
593 })
594 }
595
596 handle := func() error {
597 if err := a.contractHandler(title, wrappedAction); err != nil {
598 return err
599 }
600 return nil
601 }
602
603 // The proposer IS the authorization decision. For an authority built by
604 // NewContractAuthority it is a contractIdentityAuthority bound to
605 // contractAddr — the contract itself and nobody else; for one built by
606 // NewRestrictedContractAuthority it is whatever the consumer installed,
607 // and contractAddr is deliberately not consulted (see that constructor's
608 // godoc). `caller` is established upstream by Authorizer.DoByCurrent /
609 // DoByPrevious / Transfer under rlm.IsCurrent(), so an external realm
610 // cannot present an arbitrary principal here.
611 //
612 // A nil proposer is not a policy, it is a malformed value: both
613 // constructors always install one, so nil means the struct was built by
614 // 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}
620
621// String renders the contract path AND the proposer.
622//
623// The proposer half is security-relevant, not cosmetic: it is the only
624// thing distinguishing a gated authority from a wide-open one. Rendering
625// the path alone made
626//
627// NewContractAuthority(path, handler) // gated
628// NewRestrictedContractAuthority(path, handler, AutoAccept{}) // open to all
629//
630// byte-identical, so any consumer test asserting on this string — and
631// any on-chain reader inspecting it — was blind to the difference. A
632// consumer realm could have its authority swapped for a fully permissive
633// 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 calling
639// a.proposer.String() directly. Authority is an open interface, so a foreign
640// impl can return any text it likes — including "contract-identity". That
641// made a fully permissive authority byte-identical to the gated default
642// again, defeating the very assertions this rendering exists to support.
643// Non-canonical impls are wrapped as custom_authority[...], mirroring what
644// Authorizer.String has always done at the outer level.
645//
646// Read the `contract=` half with care: it is load-bearing only for the
647// 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}
656
657// canonicalAuthorityString renders an Authority, wrapping any
658// implementation that is not one of this package's own as
659// custom_authority[...] so a foreign impl cannot impersonate a canonical
660// one by choosing its String() text. Shared by ContractAuthority.String
661// (for the nested proposer) and Authorizer.String (for the installed
662// authority).
663func canonicalAuthorityString(auth Authority) string {
664 if auth == nil {
665 // Only reachable via a struct literal that bypassed the
666 // 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}
677
678// AutoAcceptAuthority implements an authority that accepts all actions
679// AutoAcceptAuthority is a simple authority that automatically accepts all
680// actions.
681// It can be used as a proposer authority to allow anyone to create proposals.
682type AutoAcceptAuthority struct{}
683
684func NewAutoAcceptAuthority() *AutoAcceptAuthority {
685 return &AutoAcceptAuthority{}
686}
687
688func (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
689 return action()
690}
691
692func (a *AutoAcceptAuthority) String() string {
693 return "auto_accept_authority"
694}
695
696// droppedAuthority implements an authority that denies all actions
697type droppedAuthority struct{}
698
699func NewDroppedAuthority() Authority {
700 return &droppedAuthority{}
701}
702
703func (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {
704 return errors.New("dropped authority: all actions are denied")
705}
706
707func (a *droppedAuthority) String() string {
708 return "dropped_authority"
709}