-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain_encryption_admin_test.go
More file actions
304 lines (283 loc) · 11.4 KB
/
Copy pathmain_encryption_admin_test.go
File metadata and controls
304 lines (283 loc) · 11.4 KB
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
303
304
package main
import (
"context"
"net"
"path/filepath"
"testing"
"github.com/bootjp/elastickv/internal/encryption"
"github.com/bootjp/elastickv/internal/raftengine"
etcdraftengine "github.com/bootjp/elastickv/internal/raftengine/etcd"
"github.com/bootjp/elastickv/kv"
pb "github.com/bootjp/elastickv/proto"
"github.com/bootjp/elastickv/store"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
// stubEncryptionAdminEngine satisfies the encryptionAdminEngine
// interface (Proposer + LeaderView) for tests that exercise the
// Stage 6B-2 mutator-wiring gate. The methods are nil-deref-safe
// stubs; the registration helper only stores them on the
// EncryptionAdminServer options. Tests that exercise the gate via
// a gRPC client never actually issue a Raft proposal because the
// mutator path returns ErrFailedPrecondition before reaching
// Propose() in the no-mutator case, and tests for the enabled
// case stop at status.Code inspection.
type stubEncryptionAdminEngine struct{}
func (stubEncryptionAdminEngine) Propose(context.Context, []byte) (*raftengine.ProposalResult, error) {
return &raftengine.ProposalResult{}, nil
}
func (s stubEncryptionAdminEngine) ProposeAdmin(ctx context.Context, data []byte) (*raftengine.ProposalResult, error) {
return s.Propose(ctx, data)
}
func (stubEncryptionAdminEngine) State() raftengine.State { return raftengine.StateLeader }
func (stubEncryptionAdminEngine) Leader() raftengine.LeaderInfo { return raftengine.LeaderInfo{} }
func (stubEncryptionAdminEngine) VerifyLeader(context.Context) error {
return nil
}
func (stubEncryptionAdminEngine) LinearizableRead(context.Context) (uint64, error) {
return 0, nil
}
// TestEncryptionAdminFullNodeID_DistinctPerRaftId pins the
// PR760 r1 Codex P1 regression: full_node_id must be derived from
// --raftId (per-node-stable) and NOT from the Raft group id
// (shared across replicas). An earlier wiring passed rt.spec.id,
// which makes every replica return the same CapabilityReport
// value and breaks BootstrapEncryption's
// validateWriterBatchUniqueness check. The fix routes through
// etcd.DeriveNodeID(raftId) at the call site; this test confirms
// the canonical derivation is in fact distinct for the canonical
// 3-node config (n1 / n2 / n3) the demo + Jepsen runners use.
func TestEncryptionAdminFullNodeID_DistinctPerRaftId(t *testing.T) {
seen := make(map[uint64]string)
for _, id := range []string{"n1", "n2", "n3"} {
got := etcdraftengine.DeriveNodeID(id)
if got == 0 {
t.Fatalf("DeriveNodeID(%q) returned 0; the §6.1 not-capable sentinel must never be used as a real node id", id)
}
if dup, exists := seen[got]; exists {
t.Fatalf("DeriveNodeID(%q) = DeriveNodeID(%q) = %d; full_node_id must be distinct across raftIds — BootstrapEncryption uniqueness would reject", id, dup, got)
}
seen[got] = id
}
}
func TestEncryptionAdminDefaultWriterRegistryUsesDefaultGroupStore(t *testing.T) {
t.Parallel()
defaultStore := newEncryptionAdminRegistryTestStore(t, "default")
otherStore := newEncryptionAdminRegistryTestStore(t, "other")
groups := map[uint64]*kv.ShardGroup{
1: {Store: defaultStore},
2: {Store: otherStore},
}
reg, err := encryptionAdminDefaultWriterRegistry("keys.json", groups, 1)
if err != nil {
t.Fatalf("encryptionAdminDefaultWriterRegistry: %v", err)
}
key := encryption.RegistryKey(7, encryption.NodeID16(0xCAFE))
value := encryption.EncodeRegistryValue(encryption.RegistryValue{
FullNodeID: 0xCAFE,
FirstSeenLocalEpoch: 3,
LastSeenLocalEpoch: 3,
})
if err := reg.SetRegistryRow(key, value); err != nil {
t.Fatalf("SetRegistryRow: %v", err)
}
defaultReg, err := store.WriterRegistryFor(defaultStore)
if err != nil {
t.Fatalf("WriterRegistryFor(default): %v", err)
}
got, ok, err := defaultReg.GetRegistryRow(key)
if err != nil {
t.Fatalf("GetRegistryRow(default): %v", err)
}
if !ok || string(got) != string(value) {
t.Fatalf("default registry row ok=%v value=%x, want %x", ok, got, value)
}
otherReg, err := store.WriterRegistryFor(otherStore)
if err != nil {
t.Fatalf("WriterRegistryFor(other): %v", err)
}
if got, ok, err := otherReg.GetRegistryRow(key); err != nil {
t.Fatalf("GetRegistryRow(other): %v", err)
} else if ok {
t.Fatalf("non-default registry unexpectedly has row value=%x", got)
}
}
func TestEncryptionAdminDefaultRuntimeOptionsUsesDefaultGroupRuntime(t *testing.T) {
t.Parallel()
defaultEngine := &stubEngineForProposerTest{}
otherEngine := &stubEngineForProposerTest{}
defaultGroup := &kv.ShardGroup{Engine: defaultEngine}
otherGroup := &kv.ShardGroup{Engine: otherEngine}
_ = kv.NewLeaderProxyForShardGroup(defaultGroup)
_ = kv.NewLeaderProxyForShardGroup(otherGroup)
got, err := encryptionAdminDefaultRuntimeOptions(
[]*raftGroupRuntime{
{spec: groupSpec{id: 1}, engine: defaultEngine},
{spec: groupSpec{id: 2}, engine: otherEngine},
},
map[uint64]*kv.ShardGroup{
1: defaultGroup,
2: otherGroup,
},
1,
true,
)
if err != nil {
t.Fatalf("encryptionAdminDefaultRuntimeOptions: %v", err)
}
if got.engine != encryptionAdminEngine(defaultEngine) {
t.Fatalf("engine=%T, want default group engine", got.engine)
}
if got.postCutoverProposer != defaultGroup.Proposer() {
t.Error("postCutoverProposer did not use the default group's wrap-aware proposer")
}
if got.postCutoverProposer == otherGroup.Proposer() {
t.Error("postCutoverProposer unexpectedly used the non-default group's proposer")
}
}
func TestEncryptionAdminDefaultRuntimeOptionsRequiresDefaultRuntime(t *testing.T) {
t.Parallel()
if _, err := encryptionAdminDefaultRuntimeOptions(nil, nil, 7, true); err == nil {
t.Fatal("missing required default runtime returned nil error")
}
if _, err := encryptionAdminDefaultRuntimeOptions(nil, nil, 7, false); err != nil {
t.Fatalf("missing optional default runtime returned error: %v", err)
}
}
func newEncryptionAdminRegistryTestStore(t *testing.T, name string) store.MVCCStore {
t.Helper()
st, err := store.NewPebbleStore(filepath.Join(t.TempDir(), name+".db"))
if err != nil {
t.Fatalf("NewPebbleStore(%s): %v", name, err)
}
t.Cleanup(func() {
if closer, ok := st.(interface{ Close() error }); ok {
if err := closer.Close(); err != nil {
t.Fatalf("close %s store: %v", name, err)
}
}
})
return st
}
// TestEncryptionAdmin_MutatingRPCRefusedWhenGateOff pins the
// Stage 6B-2 double-gate. With either condition of the gate false
// (enableMutators=false OR engine=nil), Proposer + LeaderView
// MUST stay unwired and EncryptionAdminServer.BootstrapEncryption
// returns FailedPrecondition at the gRPC boundary — the proposal
// never reaches Raft.
//
// The matrix below exercises all 4 corners of (enableMutators,
// engine-supplied). Only the (true, non-nil) combination is the
// production "gate ON" case, which is tested separately by
// TestEncryptionAdmin_MutatingRPCEnabledWhenGateOn.
func TestEncryptionAdmin_MutatingRPCRefusedWhenGateOff(t *testing.T) {
for _, tc := range []struct {
name string
enableMutators bool
engine encryptionAdminEngine
}{
{name: "flag_off_engine_nil", enableMutators: false, engine: nil},
{name: "flag_off_engine_set", enableMutators: false, engine: stubEncryptionAdminEngine{}},
{name: "flag_on_engine_nil", enableMutators: true, engine: nil},
} {
t.Run(tc.name, func(t *testing.T) {
err := callBootstrapAgainstNewServer(t, tc.enableMutators, tc.engine)
if err == nil {
t.Fatal("BootstrapEncryption succeeded with gate off; mutating wiring must be refused at the RPC boundary")
}
if got := status.Code(err); got != codes.FailedPrecondition {
t.Errorf("BootstrapEncryption status=%v, want FailedPrecondition; err=%v", got, err)
}
})
}
}
// TestEncryptionAdmin_MutatingRPCEnabledWhenGateOn pins the
// other side of the Stage 6B-2 gate: when enableMutators=true AND
// engine is non-nil, the mutator wiring is installed and
// BootstrapEncryption reaches the Raft propose path. The stub
// engine's Propose() returns success, so the only-RPC-layer
// portion of the path completes; deeper apply-layer behaviour is
// covered by the applier tests in internal/encryption.
//
// The expected status is NOT FailedPrecondition — that is the
// gate-off signature. The exact OK / OtherCode depends on the
// EncryptionAdminServer's downstream validation of the empty
// wrapped payload; the test asserts the FailedPrecondition gate
// is no longer firing, which is the property that distinguishes
// gate-on from gate-off.
func TestEncryptionAdmin_MutatingRPCEnabledWhenGateOn(t *testing.T) {
err := callBootstrapAgainstNewServer(t, true, stubEncryptionAdminEngine{})
// Whatever the deeper error, it must not be the FailedPrecondition
// from the gate. The stub Propose() returns success on a malformed
// payload, so the actual return may be a deeper InvalidArgument
// or codes.OK — anything except FailedPrecondition / transport
// statuses is acceptable.
if err != nil {
got := status.Code(err)
if got == codes.FailedPrecondition {
t.Errorf("BootstrapEncryption returned FailedPrecondition with gate ON; want a deeper status (gate should not refuse): err=%v", err)
}
// Bufconn transport failures or context-derived statuses
// would defeat the test purpose — fail loud rather than
// silently passing on infra noise (coderabbit PR #776
// minor).
if got == codes.Unavailable || got == codes.DeadlineExceeded || got == codes.Canceled {
t.Fatalf("BootstrapEncryption failed on test transport/setup, not gate behavior: code=%v err=%v", got, err)
}
}
}
func callBootstrapAgainstNewServer(t *testing.T, enableMutators bool, engine encryptionAdminEngine) error {
t.Helper()
listener := bufconn.Listen(1024 * 1024)
gs := grpc.NewServer()
registerEncryptionAdminServer(gs, newEncryptionAdminServer(1, "", enableMutators, engine, nil, nil, nil))
go func() { _ = gs.Serve(listener) }()
t.Cleanup(gs.Stop)
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("grpc.NewClient: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
client := pb.NewEncryptionAdminClient(conn)
_, err = client.BootstrapEncryption(context.Background(), &pb.BootstrapEncryptionRequest{
StorageDekId: 1,
RaftDekId: 2,
WrappedStorageDek: []byte("w"),
WrappedRaftDek: []byte("w"),
})
return err
}
func TestRegisterEncryptionAdminServer_Registers(t *testing.T) {
gs := grpc.NewServer()
registerEncryptionAdminServer(gs, newEncryptionAdminServer(1, "", false, nil, nil, nil, nil))
info := gs.GetServiceInfo()
if _, ok := info["EncryptionAdmin"]; !ok {
var registered []string
for name := range info {
registered = append(registered, name)
}
t.Fatalf("EncryptionAdmin not registered; got services=%v", registered)
}
}
func TestRegisterEncryptionAdminServer_CanReuseInstance(t *testing.T) {
srv := newEncryptionAdminServer(1, "", false, nil, nil, nil, nil)
first := grpc.NewServer()
second := grpc.NewServer()
registerEncryptionAdminServer(first, srv)
registerEncryptionAdminServer(second, srv)
if _, ok := first.GetServiceInfo()["EncryptionAdmin"]; !ok {
t.Fatal("EncryptionAdmin service not registered on first gRPC server")
}
if _, ok := second.GetServiceInfo()["EncryptionAdmin"]; !ok {
t.Fatal("EncryptionAdmin service not registered on second gRPC server")
}
}