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}