carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package appconfig
2
3import (
4 "context"
5 "crypto/ecdh"
6 "encoding/json"
7 "errors"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// Issue #213: a box takes an instance's lease only if it can provide that
15// instance's config. These tests pin the predicate the activator and the
16// reconcile pass both ask.
17
18type canProvideEnv struct {
19 t *testing.T
20 s release.Store
21 priv *ecdh.PrivateKey
22 pub *ecdh.PublicKey
23 other *ecdh.PrivateKey // a key this deployment never sealed for
24}
25
26func newCanProvideEnv(t *testing.T) *canProvideEnv {
27 t.Helper()
28 priv, err := ecdh.P256().GenerateKey(randReader)
29 if err != nil {
30 t.Fatal(err)
31 }
32 other, err := ecdh.P256().GenerateKey(randReader)
33 if err != nil {
34 t.Fatal(err)
35 }
36 return &canProvideEnv{t: t, s: release.NewDirStore(t.TempDir()), priv: priv, pub: priv.PublicKey(), other: other}
37}
38
39func (e *canProvideEnv) put(key string, m Manifest) {
40 e.t.Helper()
41 m.Schema = 1
42 body, err := json.Marshal(m)
43 if err != nil {
44 e.t.Fatal(err)
45 }
46 if _, err := e.s.Put(context.Background(), key, body, ""); err != nil {
47 e.t.Fatal(err)
48 }
49}
50
51func (e *canProvideEnv) secret(plain string) Var {
52 e.t.Helper()
53 env, err := SealUnbound(e.pub, []byte(plain))
54 if err != nil {
55 e.t.Fatal(err)
56 }
57 return Var{Kind: "secret", Envelope: env}
58}
59
60func reasonOf(t *testing.T, err error) string {
61 t.Helper()
62 var r *ConfigRefusal
63 if !errors.As(err, &r) {
64 t.Fatalf("err = %v, want a *ConfigRefusal", err)
65 }
66 return r.Reason
67}
68
69func TestCanProvideConfigNothingSealedNeedsNothing(t *testing.T) {
70 e := newCanProvideEnv(t)
71 ctx := context.Background()
72 // No manifest anywhere: a dev deployment. No key either.
73 if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com"); err != nil {
74 t.Fatalf("no manifest, no key: %v", err)
75 }
76 // A manifest with plain vars only has no sealed config.
77 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
78 if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com"); err != nil {
79 t.Fatalf("plain-only manifest, no key: %v", err)
80 }
81 // A manifest that was fully unset exists but is empty.
82 e.put(Key("bab", "other"), Manifest{App: "other", Vars: map[string]Var{}})
83 if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "other", "", "a.example.com"); err != nil {
84 t.Fatalf("empty manifest, no key: %v", err)
85 }
86}
87
88func TestCanProvideConfigSealedNeedsAKeyThatOpensIt(t *testing.T) {
89 e := newCanProvideEnv(t)
90 ctx := context.Background()
91 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": e.secret("pw")}})
92
93 if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com"); err != nil {
94 t.Fatalf("right key: %v", err)
95 }
96 if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com")); got != ReasonNoKey {
97 t.Errorf("absent key reason = %q, want %q", got, ReasonNoKey)
98 }
99 if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyUnparseable, "bab", "shop", "", "a.example.com")); got != ReasonKeyUnparseable {
100 t.Errorf("unparseable key reason = %q, want %q", got, ReasonKeyUnparseable)
101 }
102 if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.other, KeyOK, "bab", "shop", "", "a.example.com")); got != ReasonCannotOpen {
103 t.Errorf("wrong key reason = %q, want %q", got, ReasonCannotOpen)
104 }
105}
106
107// The instance overlay and the bound environment are part of what the box
108// renders, so a secret only there still needs the key.
109func TestCanProvideConfigLooksAtEveryLayerTheBoxRenders(t *testing.T) {
110 e := newCanProvideEnv(t)
111 ctx := context.Background()
112 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
113 e.put(EnvironmentKey("bab", "shop", "staging"), Manifest{App: "shop", Vars: map[string]Var{"S": e.secret("x")}})
114 e.put(InstanceConfigKey("bab", "shop", "cust.example.com"), Manifest{App: "shop", Vars: map[string]Var{"T": e.secret("y")}})
115
116 // Unbound route, no overlay for this host: nothing sealed.
117 if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "plain.example.com"); err != nil {
118 t.Errorf("unbound plain host: %v", err)
119 }
120 // Bound to the environment: sealed.
121 if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "staging", "plain.example.com")); got != ReasonNoKey {
122 t.Errorf("environment secret reason = %q", got)
123 }
124 // The overlay for cust.example.com only.
125 if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "cust.example.com")); got != ReasonNoKey {
126 t.Errorf("overlay secret reason = %q", got)
127 }
128 if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "cust.example.com"); err != nil {
129 t.Errorf("overlay with the key: %v", err)
130 }
131}
132
133// SyncExec writes the WHOLE config file or none of it (RenderEnv fails if any
134// one decrypt fails), and instanceEnv accepts a missing file. So a manifest
135// that is only partly openable on a box with no rendered file would start the
136// instance with none of its sealed config: fresh admission needs every
137// envelope to open.
138func TestCanProvideConfigRefusesPartlyOpenableConfigOnFreshAdmission(t *testing.T) {
139 ctx := context.Background()
140 cases := map[string]func(e *canProvideEnv, foreign string) map[string]Var{
141 "one foreign-key envelope": func(e *canProvideEnv, f string) map[string]Var {
142 return map[string]Var{"A": {Kind: "secret", Envelope: f}, "C": e.secret("ok")}
143 },
144 "one corrupt envelope": func(e *canProvideEnv, f string) map[string]Var {
145 return map[string]Var{"B": {Kind: "secret", Envelope: "not-base64!!"}, "C": e.secret("ok")}
146 },
147 "an unwritable plain value": func(e *canProvideEnv, f string) map[string]Var {
148 return map[string]Var{"P": {Kind: "env", Value: "has \"quote"}, "C": e.secret("ok")}
149 },
150 "all foreign": func(e *canProvideEnv, f string) map[string]Var {
151 return map[string]Var{"A": {Kind: "secret", Envelope: f}}
152 },
153 }
154 for name, mk := range cases {
155 t.Run(name, func(t *testing.T) {
156 e := newCanProvideEnv(t)
157 foreign, err := SealUnbound(e.other.PublicKey(), []byte("z"))
158 if err != nil {
159 t.Fatal(err)
160 }
161 vars := mk(e, foreign)
162 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: vars})
163 if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com")); got != ReasonCannotOpen {
164 t.Errorf("reason = %q, want cannot_open", got)
165 }
166 })
167 }
168}
169
170// A v2 envelope is bound to its (account, app): the guard opens it under the
171// route's own binding, as the sync pass does.
172func TestCanProvideConfigOpensBoundEnvelopesUnderTheRoutesBinding(t *testing.T) {
173 e := newCanProvideEnv(t)
174 ctx := context.Background()
175 env, err := SealBound(e.pub, Binding{Account: "bab", App: "shop"}, []byte("pw"))
176 if err != nil {
177 t.Fatal(err)
178 }
179 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": {Kind: "secret", Envelope: env}}})
180 if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com"); err != nil {
181 t.Fatalf("bound envelope, own app: %v", err)
182 }
183 // The same bytes copied under another app do not open there.
184 e.put(Key("bab", "thief"), Manifest{App: "thief", Vars: map[string]Var{"DB": {Kind: "secret", Envelope: env}}})
185 if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "thief", "", "a.example.com")); got != ReasonCannotOpen {
186 t.Errorf("copied bound envelope reason = %q", got)
187 }
188}
189
190// failingStore fails reads of the keys it is told to, and delegates the rest.
191type failingStore struct {
192 release.Store
193 failGet map[string]error
194}
195
196func (f *failingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
197 if err, ok := f.failGet[key]; ok {
198 return nil, "", err
199 }
200 return f.Store.Get(ctx, key)
201}
202
203// "We could not tell" is never "there is no sealed config": a read error on
204// any layer refuses, with or without a key.
205func TestCanProvideConfigFailsClosedWhenItCannotRead(t *testing.T) {
206 e := newCanProvideEnv(t)
207 ctx := context.Background()
208 boom := errors.New("s3: 503 slow down")
209 e.put(EnvironmentKey("bab", "shop", "staging"), Manifest{App: "shop", Vars: map[string]Var{}})
210 for name, key := range map[string]string{
211 "default": Key("bab", "shop"),
212 "environment": EnvironmentKey("bab", "shop", "staging"),
213 "overlay": InstanceConfigKey("bab", "shop", "a.example.com"),
214 } {
215 t.Run(name, func(t *testing.T) {
216 fs := &failingStore{Store: e.s, failGet: map[string]error{key: boom}}
217 for _, k := range []struct {
218 priv *ecdh.PrivateKey
219 st KeyState
220 }{{e.priv, KeyOK}, {nil, KeyAbsent}} {
221 err := CanProvideConfig(ctx, fs, k.priv, k.st, "bab", "shop", "staging", "a.example.com")
222 if got := reasonOf(t, err); got != ReasonStoreError {
223 t.Errorf("reason = %q, want %q", got, ReasonStoreError)
224 }
225 if !errors.Is(err, boom) {
226 t.Errorf("err %v does not wrap the store error", err)
227 }
228 }
229 })
230 }
231}
232
233// A manifest the box cannot parse and a bound environment that is gone are
234// both "cannot tell".
235func TestCanProvideConfigFailsClosedOnUnreadableConfig(t *testing.T) {
236 e := newCanProvideEnv(t)
237 ctx := context.Background()
238 if _, err := e.s.Put(ctx, Key("bab", "garbled"), []byte("{not json"), ""); err != nil {
239 t.Fatal(err)
240 }
241 if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "garbled", "", "a.example.com")); got != ReasonStoreError {
242 t.Errorf("garbled manifest reason = %q", got)
243 }
244 e.put(Key("bab", "bound"), Manifest{App: "bound", Vars: map[string]Var{}})
245 if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "bound", "gone", "a.example.com")); got != ReasonStoreError {
246 t.Errorf("missing bound environment reason = %q", got)
247 }
248}
249
250// A host that fails ValidInstanceHost cannot have an overlay (the sweeps read
251// that as "no overlay"); the guard must answer, not error on the host shape.
252func TestCanProvideConfigHostWithNoPossibleOverlay(t *testing.T) {
253 e := newCanProvideEnv(t)
254 ctx := context.Background()
255 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
256 if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "Mixed.Case.example.com"); err != nil {
257 t.Fatalf("host without a possible overlay: %v", err)
258 }
259}
260
261// Nothing secret leaves in the error: not the envelope, not the plaintext.
262func TestConfigRefusalReasonIsAClosedWord(t *testing.T) {
263 e := newCanProvideEnv(t)
264 ctx := context.Background()
265 v := e.secret("hunter2")
266 e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": v}})
267 err := CanProvideConfig(ctx, e.s, e.other, KeyOK, "bab", "shop", "", "a.example.com")
268 var r *ConfigRefusal
269 if !errors.As(err, &r) {
270 t.Fatal(err)
271 }
272 switch r.Reason {
273 case ReasonNoKey, ReasonKeyUnparseable, ReasonCannotOpen, ReasonStoreError, ReasonNoSource:
274 default:
275 t.Errorf("reason %q is not one of the closed words", r.Reason)
276 }
277 if strings.Contains(err.Error(), v.Envelope) || strings.Contains(err.Error(), "hunter2") {
278 t.Errorf("error leaks sealed material: %v", err)
279 }
280}
281