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

proxy.gno

6.92 Kb · 206 lines
  1package config
  2
  3import (
  4	"strings"
  5
  6	"chain"
  7
  8	"gno.land/p/nt/avl/v0"
  9)
 10
 11// Version chaining: how v2 and v3 get built without splitting the state.
 12//
 13// # The shape, and why it is this way round
 14//
 15// This realm is public, so its path is permanent and a new API means a new
 16// version at a new path. Left alone, that fragments everything: a realm
 17// importing v1 and a realm importing v2 would read two different member lists
 18// and two different pause switches, and the second one to be written would win
 19// for half the callers.
 20//
 21// Delegation fixes it, and it can only run one way. A version can import the
 22// versions that already existed when it was written; it can never import one
 23// that does not exist yet. So the STATE STAYS IN THE OLDEST version that has
 24// it, and every later version is a thin relay in front:
 25//
 26//	v3  ->  v2  ->  v1        (this one: the root, and where everything lives)
 27//
 28// A realm importing v1, v2 or v3 is then reading the same settings, the same
 29// pause and the same managers, whichever door it came through. That is the
 30// property worth all of this.
 31//
 32// v0 cannot take part: it is already on chain and has no settings store at
 33// all. It keeps answering for its own member list and nothing else.
 34//
 35// # Writing v2
 36//
 37// v2 imports this package, holds no state of its own, and for each write it
 38// exposes, calls SetAs or UnsetAs with the address it was called by:
 39//
 40//	func Set(cur realm, key, value string) {
 41//		config.SetAs(cross(cur), cur.Previous().Address(), key, value)
 42//	}
 43//
 44// Reads need none of this. They are plain borrowed calls, so v2 just forwards:
 45//
 46//	func Get(key string) string { return config.Get(key) }
 47//
 48// Then, once, from a manager's address:
 49//
 50//	AllowProxy gno.land/r/moul/config/v2
 51//
 52// # What a proxy is trusted with, exactly
 53//
 54// A registered proxy is trusted to say WHO is asking, not to decide whether
 55// they may. SetAs still puts the principal through the Authorizer, so the
 56// member list stays the single answer to "who may change config" for every
 57// version at once, and adding a manager here works through v2 and v3 with no
 58// further deploys.
 59//
 60// It is not a security boundary against the proxy's own code: a proxy that
 61// lies about its principal writes whatever it likes. It does not have to be,
 62// because the same person deploys both and registering one is a deliberate,
 63// revocable act. What it buys is that v2 never carries a copy of the member
 64// list, so the two can never disagree.
 65
 66// proxies holds the package paths allowed to relay, keyed by path, valued by
 67// the address that path resolves to. The address is what an incoming call is
 68// actually compared against; the path is stored so Render and ListProxies can
 69// show something a human recognises.
 70var proxies = avl.NewTree() // pkgPath -> address
 71
 72// AllowProxy registers a newer version of this realm as a relay. Manager only.
 73//
 74// It takes a package path rather than an address because a path is what a
 75// manager can check by eye; the address is derived here with the same function
 76// the chain uses, so the two cannot drift.
 77func AllowProxy(cur realm, pkgPath string) {
 78	assertProxyPath(pkgPath)
 79	assertAuthorized(cur, "config.AllowProxy:"+pkgPath)
 80
 81	proxies.Set(pkgPath, chain.PackageAddress(pkgPath))
 82	settingsRev++
 83	chain.Emit("ConfigAllowProxy", "path", pkgPath,
 84		"address", chain.PackageAddress(pkgPath).String())
 85}
 86
 87// RevokeProxy withdraws a relay. Manager only.
 88//
 89// Revoking one that was never registered aborts rather than passing quietly:
 90// at this point in an incident, "done" and "you misspelled it" must not look
 91// the same.
 92func RevokeProxy(cur realm, pkgPath string) {
 93	assertProxyPath(pkgPath)
 94	assertAuthorized(cur, "config.RevokeProxy:"+pkgPath)
 95
 96	if _, removed := proxies.Remove(pkgPath); !removed {
 97		panic("no such proxy: " + pkgPath)
 98	}
 99	settingsRev++
100	chain.Emit("ConfigRevokeProxy", "path", pkgPath)
101}
102
103// IsProxy reports whether addr is a registered relay.
104func IsProxy(addr address) bool {
105	if addr == "" {
106		return false
107	}
108	found := false
109	proxies.Iterate("", "", func(_ string, value any) bool {
110		if value.(address) == addr {
111			found = true
112			return true // stop
113		}
114		return false
115	})
116	return found
117}
118
119// ListProxies returns the registered relay paths, in sorted order.
120func ListProxies() []string {
121	out := []string{}
122	proxies.Iterate("", "", func(key string, _ any) bool {
123		out = append(out, key)
124		return false
125	})
126	return out
127}
128
129// SetAs is Set, performed by a registered proxy on behalf of principal.
130//
131// Two checks, and both have to hold: the caller is a registered relay, and the
132// principal it names is authorized. Neither is redundant. Dropping the first
133// would let any realm claim any principal; dropping the second would make a
134// registered proxy an unconditional bypass of the member list.
135func SetAs(cur realm, principal address, key, value string) {
136	assertRelay(cur)
137	assertWritableKey(key)
138	assertValue(key, value)
139	assertPrincipal(principal, "config.Set:"+key)
140	writeSetting(key, value)
141}
142
143// UnsetAs is Unset, performed by a registered proxy on behalf of principal.
144func UnsetAs(cur realm, principal address, key string) {
145	assertRelay(cur)
146	assertWritableKey(key)
147	assertPrincipal(principal, "config.Unset:"+key)
148	removeSetting(key)
149}
150
151// assertRelay aborts unless the immediate caller is a registered proxy.
152func assertRelay(cur realm) {
153	from := cur.Previous().Address()
154	if !IsProxy(from) {
155		panic("not a registered proxy: " + from.String())
156	}
157}
158
159// assertPrincipal puts an address through the Authorizer without it being the
160// caller, which is what a relay needs and what DoByPrevious cannot express.
161//
162// With the default MemberAuthority this is a membership test. With an
163// authority that inspects the caller instead (a ContractAuthority), the
164// relayed path asks that authority about the principal, so check what it does
165// before transferring management while a proxy is registered.
166func assertPrincipal(principal address, title string) {
167	if principal == "" {
168		panic("unauthorized: no principal")
169	}
170	allowed := false
171	err := Authorizer.Authority().Authorize(principal, title, func() error {
172		allowed = true
173		return nil
174	})
175	if err != nil {
176		panic("unauthorized: " + err.Error())
177	}
178	if !allowed {
179		panic("unauthorized")
180	}
181}
182
183// assertProxyPath refuses anything that is not a realm path under this
184// namespace. A proxy is by definition a later version of this realm, so
185// nothing else should ever be registered, and the narrow check turns a
186// fat-fingered AllowProxy into an abort rather than a standing grant to an
187// unrelated realm.
188func assertProxyPath(pkgPath string) {
189	const want = "gno.land/r/moul/config/v"
190	if !strings.HasPrefix(pkgPath, want) {
191		panic("a proxy must be a later version of this realm, got " + pkgPath)
192	}
193	rest := strings.TrimPrefix(pkgPath, want)
194	if rest == "" || !isDigits(rest) {
195		panic("a proxy must be a later version of this realm, got " + pkgPath)
196	}
197}
198
199func isDigits(s string) bool {
200	for i := 0; i < len(s); i++ {
201		if s[i] < '0' || s[i] > '9' {
202			return false
203		}
204	}
205	return len(s) > 0
206}