-
Notifications
You must be signed in to change notification settings - Fork 368
/
permissions.go
302 lines (254 loc) · 9.11 KB
/
permissions.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
package types
import (
"encoding/json"
fmt "fmt"
"reflect"
"strings"
"github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramsproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
proto "github.com/gogo/protobuf/proto"
)
func init() {
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
// But since these proposals contain Permissions, these types also need registering:
govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil)
govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission")
govtypes.RegisterProposalTypeCodec(TextPermission{}, "kava/TextPermission")
govtypes.RegisterProposalTypeCodec(SoftwareUpgradePermission{}, "kava/SoftwareUpgradePermission")
govtypes.RegisterProposalTypeCodec(ParamsChangePermission{}, "kava/ParamsChangePermission")
}
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface {
Allows(sdk.Context, ParamKeeper, PubProposal) bool
}
func PackPermissions(permissions []Permission) ([]*types.Any, error) {
permissionsAny := make([]*types.Any, len(permissions))
for i, permission := range permissions {
msg, ok := permission.(proto.Message)
if !ok {
return nil, fmt.Errorf("cannot proto marshal %T", permission)
}
any, err := types.NewAnyWithValue(msg)
if err != nil {
return nil, err
}
permissionsAny[i] = any
}
return permissionsAny, nil
}
func UnpackPermissions(permissionsAny []*types.Any) ([]Permission, error) {
permissions := make([]Permission, len(permissionsAny))
for i, any := range permissionsAny {
permission, ok := any.GetCachedValue().(Permission)
if !ok {
return nil, fmt.Errorf("expected base committee permission")
}
permissions[i] = permission
}
return permissions, nil
}
var (
_ Permission = GodPermission{}
_ Permission = TextPermission{}
_ Permission = SoftwareUpgradePermission{}
_ Permission = ParamsChangePermission{}
)
// Allows implement permission interface for GodPermission.
func (GodPermission) Allows(sdk.Context, ParamKeeper, PubProposal) bool { return true }
// Allows implement permission interface for TextPermission.
func (TextPermission) Allows(_ sdk.Context, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(*govtypes.TextProposal)
return ok
}
// Allows implement permission interface for SoftwareUpgradePermission.
func (SoftwareUpgradePermission) Allows(_ sdk.Context, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(*upgradetypes.SoftwareUpgradeProposal)
return ok
}
// Allows implement permission interface for ParamsChangePermission.
func (perm ParamsChangePermission) Allows(ctx sdk.Context, pk ParamKeeper, p PubProposal) bool {
proposal, ok := p.(*paramsproposal.ParameterChangeProposal)
if !ok {
return false
}
// Check if all proposal changes are allowed by this permission.
for _, change := range proposal.Changes {
targetedParamsChange := perm.AllowedParamsChanges.filterByParamChange(change)
// We allow the proposal param change if any of the targeted AllowedParamsChange allows it.
// This give the option of having multiple rules for the same subspace/key if needed.
allowed := false
for _, pc := range targetedParamsChange {
if pc.allowsParamChange(ctx, change, pk) {
allowed = true
break
}
}
// If no target param change allows the proposed change, then the proposal is rejected.
if !allowed {
return false
}
}
return true
}
type AllowedParamsChanges []AllowedParamsChange
// Get searches the allowedParamsChange slice for the first item matching a subspace and key.
// It returns false if not found.
func (changes AllowedParamsChanges) Get(subspace, key string) (AllowedParamsChange, bool) {
for _, apc := range changes {
if apc.Subspace == subspace && apc.Key == key {
return apc, true
}
}
return AllowedParamsChange{}, false
}
// Set adds a new AllowedParamsChange, overwriting the first exiting item with matching subspace and key.
func (changes *AllowedParamsChanges) Set(newChange AllowedParamsChange) {
for i, apc := range *changes {
if apc.Subspace == newChange.Subspace && apc.Key == newChange.Key {
(*changes)[i] = newChange
return
}
}
*changes = append(*changes, newChange)
}
// Delete removes the first AllowedParamsChange matching subspace and key.
func (changes *AllowedParamsChanges) Delete(subspace, key string) {
var found bool
var foundAt int
for i, apc := range *changes {
if apc.Subspace == subspace && apc.Key == key {
found = true
foundAt = i
break
}
}
if !found {
return
}
*changes = append(
(*changes)[:foundAt],
(*changes)[foundAt+1:]...,
)
}
// filterByParamChange returns all targeted AllowedParamsChange that matches a given ParamChange's subspace and key.
func (changes AllowedParamsChanges) filterByParamChange(paramChange paramsproposal.ParamChange) AllowedParamsChanges {
filtered := []AllowedParamsChange{}
for _, p := range changes {
if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key {
filtered = append(filtered, p)
}
}
return filtered
}
// SubparamChanges is a map of sub param change keys and its values.
type SubparamChanges map[string]interface{}
// MultiSubparamChanges is a slice of SubparamChanges.
type MultiSubparamChanges []SubparamChanges
func (allowed AllowedParamsChange) allowsMultiParamsChange(currentRecords MultiSubparamChanges, incomingRecords MultiSubparamChanges) bool {
// do not allow new records from being added or removed for multi-subparam changes.
if len(currentRecords) != len(incomingRecords) {
return false
}
for _, current := range currentRecords {
// find the incoming record and the requirements for each current record
var req *SubparamRequirement
var incoming *SubparamChanges
for _, v := range allowed.MultiSubparamsRequirements {
if current[v.Key] == v.Val {
req = &v
break
}
}
// all records should have a requirement, otherwise the change is not allowed
if req == nil {
return false
}
for _, v := range incomingRecords {
if v[req.Key] == req.Val {
incoming = &v
break
}
}
// disallow the change if no incoming record found for current record.
if incoming == nil {
return false
}
// check incoming changes are allowed
allowed := validateParamChangesAreAllowed(current, *incoming, req.AllowedSubparamAttrChanges)
if !allowed {
return false
}
}
return true
}
func validateParamChangesAreAllowed(current SubparamChanges, incoming SubparamChanges, allowList []string) bool {
// make sure we are not adding or removing any new attributes
if len(current) != len(incoming) {
return false
}
// Warning: ranging over maps iterates through keys in a random order.
// All state machine code must be deterministic between validators.
// This function's output is deterministic despite the range.
for k, v := range current {
isAllowed := false
// check if the param attr key is in the allow list
for _, allowedKey := range allowList {
if k == allowedKey {
isAllowed = true
break
}
}
// if not allowed, incoming value needs to be the same, or it is rejected
if !isAllowed && !reflect.DeepEqual(v, incoming[k]) {
return false
}
}
return true
}
func (allowed AllowedParamsChange) allowsSingleParamsChange(current SubparamChanges, incoming SubparamChanges) bool {
return validateParamChangesAreAllowed(current, incoming, allowed.SingleSubparamAllowedAttrs)
}
// allowsParamChange returns true if the given proposal param change is allowed by the AllowedParamsChange rules.
func (allowed AllowedParamsChange) allowsParamChange(ctx sdk.Context, paramsChange paramsproposal.ParamChange, pk ParamKeeper) bool {
// Check if param change matches target subspace and key.
if allowed.Subspace != paramsChange.Subspace && allowed.Key != paramsChange.Key {
return false
}
// Allow all param changes if no subparam rules are specified.
if len(allowed.SingleSubparamAllowedAttrs) == 0 && len(allowed.MultiSubparamsRequirements) == 0 {
return true
}
subspace, found := pk.GetSubspace(paramsChange.Subspace)
if !found {
return false
}
currentRaw := subspace.GetRaw(ctx, []byte(paramsChange.Key))
// Check if current param value is an array before unmarshalling to corresponding types
tdata := strings.TrimLeft(string(currentRaw), "\t\r\n")
isArray := len(tdata) > 0 && tdata[0] == '['
// Handle multi param value validation
if isArray {
var changeValue MultiSubparamChanges
if err := json.Unmarshal([]byte(paramsChange.Value), &changeValue); err != nil {
return false
}
var currentValue MultiSubparamChanges
if err := json.Unmarshal(currentRaw, ¤tValue); err != nil {
panic(err)
}
return allowed.allowsMultiParamsChange(currentValue, changeValue)
}
// Handle single param value validation
var changeValue SubparamChanges
if err := json.Unmarshal([]byte(paramsChange.Value), &changeValue); err != nil {
return false
}
var currentValue SubparamChanges
if err := json.Unmarshal(currentRaw, ¤tValue); err != nil {
panic(err)
}
return allowed.allowsSingleParamsChange(currentValue, changeValue)
}