authz.gno
28.72 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/moul/addrset/v0"
43 "gno.land/p/moul/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}