master
go 308 lines 10.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretstore_test
4
5 import (
6 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "runtime"
11 "testing"
12
13 "github.com/netdata/netdata/go/plugins/logger"
14 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
15 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/backends"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18 "gopkg.in/yaml.v2"
19 )
20
21 func TestServiceRegistryMetadata(t *testing.T) {
22 svc := secretstore.NewService(backends.Creators()...)
23
24 assert.Equal(t, []secretstore.StoreKind{secretstore.KindAWSSM, secretstore.KindAzureKV, secretstore.KindGCPSM, secretstore.KindVault}, svc.Kinds())
25
26 name, ok := svc.DisplayName(secretstore.KindVault)
27 require.True(t, ok)
28 assert.Equal(t, "Vault", name)
29
30 schema, ok := svc.Schema(secretstore.KindVault)
31 require.True(t, ok)
32 schemaObject := decodeSchema(t, schema)
33 jsonSchema, ok := schemaObject["jsonSchema"].(map[string]any)
34 require.True(t, ok)
35 _, ok = jsonSchema["properties"].(map[string]any)["kind"]
36 assert.False(t, ok)
37 _, ok = schemaObject["uiSchema"].(map[string]any)
38 assert.True(t, ok)
39 }
40
41 func TestServiceStatusAndGenerationLifecycle(t *testing.T) {
42 svc := secretstore.NewService(backends.Creators()...)
43
44 config := testSingleVaultConfig()
45 err := svc.Add(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, config))
46 require.NoError(t, err)
47 assert.Equal(t, uint64(1), svc.Capture().Generation())
48
49 storeKey := secretstore.StoreKey(secretstore.KindVault, "vault_prod")
50 status, ok := svc.GetStatus(storeKey)
51 require.True(t, ok)
52 assert.Equal(t, "vault_prod", status.Name)
53 assert.Equal(t, secretstore.KindVault, status.Kind)
54 assert.Nil(t, status.LastValidation)
55
56 runtimeUpdate := testSingleVaultConfig()
57 runtimeUpdate["mode"] = "token_file"
58 runtimeUpdate["mode_token"] = nil
59 runtimeUpdate["mode_token_file"] = map[string]any{
60 "path": "/var/lib/netdata/vault.token",
61 }
62 err = svc.Update(context.Background(), storeKey, newStoreFromConfig(t, svc, secretstore.KindVault, runtimeUpdate))
63 require.NoError(t, err)
64 assert.Equal(t, uint64(2), svc.Capture().Generation())
65
66 err = svc.Update(context.Background(), storeKey, newStoreFromConfig(t, svc, secretstore.KindVault, runtimeUpdate))
67 require.NoError(t, err)
68 assert.Equal(t, uint64(2), svc.Capture().Generation())
69
70 err = svc.ValidateStored(context.Background(), storeKey)
71 require.NoError(t, err)
72
73 status, ok = svc.GetStatus(storeKey)
74 require.True(t, ok)
75 require.NotNil(t, status.LastValidation)
76 assert.True(t, status.LastValidation.OK)
77 }
78
79 func TestServiceUpdate_UnknownFieldOnlyChangeCountsAsChange(t *testing.T) {
80 svc := secretstore.NewService(backends.Creators()...)
81
82 base := testSingleVaultConfig()
83 err := svc.Add(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, base))
84 require.NoError(t, err)
85 assert.Equal(t, uint64(1), svc.Capture().Generation())
86
87 changed := testSingleVaultConfig()
88 changed["ui_note"] = "kept"
89 err = svc.Update(context.Background(), secretstore.StoreKey(secretstore.KindVault, "vault_prod"), newStoreFromConfig(t, svc, secretstore.KindVault, changed))
90 require.NoError(t, err)
91 assert.Equal(t, uint64(2), svc.Capture().Generation())
92 }
93
94 func TestServiceValidateAcceptsYAMLDecodedNestedMaps(t *testing.T) {
95 svc := secretstore.NewService(backends.Creators()...)
96
97 var cfg secretstore.Config
98 require.NoError(t, yaml.Unmarshal([]byte(`
99 mode: token
100 mode_token:
101 token: vault-token
102 addr: https://vault.example
103 `), &cfg))
104 cfg.SetName("vault_prod")
105 cfg.SetKind(secretstore.KindVault)
106 cfg.SetSource("dyncfg")
107 cfg.SetSourceType("dyncfg")
108
109 require.NoError(t, svc.Validate(context.Background(), cfg))
110 }
111
112 func TestServiceUsesSentinelErrors(t *testing.T) {
113 svc := secretstore.NewService(backends.Creators()...)
114
115 err := svc.Add(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, testSingleVaultConfig()))
116 require.NoError(t, err)
117
118 err = svc.Add(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, testSingleVaultConfig()))
119 require.Error(t, err)
120 assert.ErrorIs(t, err, secretstore.ErrStoreExists)
121
122 missing := testSingleVaultConfig()
123 missing["name"] = "missing"
124 err = svc.Update(context.Background(), secretstore.StoreKey(secretstore.KindVault, "missing"), newStoreFromConfig(t, svc, secretstore.KindVault, missing))
125 require.Error(t, err)
126 assert.ErrorIs(t, err, secretstore.ErrStoreNotFound)
127
128 err = svc.Remove(secretstore.StoreKey(secretstore.KindVault, "missing"))
129 require.Error(t, err)
130 assert.True(t, errors.Is(err, secretstore.ErrStoreNotFound))
131 }
132
133 func TestProviderBackedValidationContracts(t *testing.T) {
134 svc := secretstore.NewService(backends.Creators()...)
135
136 err := svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindAWSSM, map[string]any{
137 "name": "aws_prod",
138 "auth_mode": "env",
139 }))
140 require.Error(t, err)
141 assert.ErrorContains(t, err, "region is required")
142
143 err = svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, map[string]any{
144 "name": "vault_prod",
145 "mode": "token",
146 "mode_token": map[string]any{
147 "token": "vault-token",
148 },
149 }))
150 require.Error(t, err)
151 assert.ErrorContains(t, err, "addr is required")
152
153 awsSchema, ok := svc.Schema(secretstore.KindAWSSM)
154 require.True(t, ok)
155 awsSchemaObject := decodeSchema(t, awsSchema)
156 awsJSONSchema, ok := awsSchemaObject["jsonSchema"].(map[string]any)
157 require.True(t, ok)
158 assert.Contains(t, awsJSONSchema["required"], "auth_mode")
159 assert.NotContains(t, awsJSONSchema["required"], "kind")
160 assert.Contains(t, awsJSONSchema["required"], "region")
161
162 vaultSchema, ok := svc.Schema(secretstore.KindVault)
163 require.True(t, ok)
164 vaultSchemaObject := decodeSchema(t, vaultSchema)
165 vaultJSONSchema, ok := vaultSchemaObject["jsonSchema"].(map[string]any)
166 require.True(t, ok)
167 assert.Contains(t, vaultJSONSchema["required"], "addr")
168 }
169
170 func TestProviderBackedAddAcrossKinds(t *testing.T) {
171 svc := secretstore.NewService(backends.Creators()...)
172
173 for _, entry := range providerBackedConfigs() {
174 t.Run(string(entry.kind), func(t *testing.T) {
175 err := svc.Add(context.Background(), newStoreFromConfig(t, svc, entry.kind, entry.config))
176 require.NoError(t, err)
177
178 status, ok := svc.GetStatus(secretstore.StoreKey(entry.kind, entry.name))
179 require.True(t, ok)
180 assert.Equal(t, entry.kind, status.Kind)
181
182 err = svc.ValidateStored(context.Background(), secretstore.StoreKey(entry.kind, entry.name))
183 require.NoError(t, err)
184 })
185 }
186 }
187
188 func TestServiceValidate_ResolvesBuiltinSecretsInProviderPayload(t *testing.T) {
189 t.Setenv("TEST_VAULT_MODE", "token")
190
191 modeFile := filepath.Join(t.TempDir(), "vault-mode")
192 require.NoError(t, os.WriteFile(modeFile, []byte("token\n"), 0o644))
193
194 tests := map[string]struct {
195 modeRef string
196 onWindowsSkip bool
197 }{
198 "env": {
199 modeRef: "${env:TEST_VAULT_MODE}",
200 },
201 "file": {
202 modeRef: "${file:" + modeFile + "}",
203 },
204 "cmd": {
205 modeRef: "${cmd:/bin/echo token}",
206 onWindowsSkip: true,
207 },
208 }
209
210 for name, tc := range tests {
211 t.Run(name, func(t *testing.T) {
212 if tc.onWindowsSkip && runtime.GOOS == "windows" {
213 t.Skip("skipping on windows")
214 }
215
216 svc := secretstore.NewService(backends.Creators()...)
217
218 cfg := testSingleVaultConfig()
219 cfg["mode"] = tc.modeRef
220
221 require.NoError(t, svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, cfg)))
222 })
223 }
224 }
225
226 func TestServiceValidate_ResolvesBuiltinSecretsInNestedProviderPayload(t *testing.T) {
227 t.Setenv("TEST_VAULT_TOKEN", "vault-token")
228
229 svc := secretstore.NewService(backends.Creators()...)
230
231 cfg := testSingleVaultConfig()
232 cfg["mode_token"] = map[string]any{
233 "token": "${env:TEST_VAULT_TOKEN}",
234 }
235
236 require.NoError(t, svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, cfg)))
237 }
238
239 func TestServiceAddUpdate_ResolvesBuiltinSecretsInProviderPayload(t *testing.T) {
240 t.Setenv("TEST_VAULT_MODE", "token")
241
242 modeFile := filepath.Join(t.TempDir(), "vault-mode")
243 require.NoError(t, os.WriteFile(modeFile, []byte("token\n"), 0o644))
244
245 svc := secretstore.NewService(backends.Creators()...)
246 storeKey := secretstore.StoreKey(secretstore.KindVault, "vault_prod")
247
248 envCfg := testSingleVaultConfig()
249 envCfg["mode"] = "${env:TEST_VAULT_MODE}"
250 require.NoError(t, svc.Add(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, envCfg)))
251 assert.Equal(t, uint64(1), svc.Capture().Generation())
252
253 fileCfg := testSingleVaultConfig()
254 fileCfg["mode"] = "${file:" + modeFile + "}"
255 require.NoError(t, svc.Update(context.Background(), storeKey, newStoreFromConfig(t, svc, secretstore.KindVault, fileCfg)))
256 assert.Equal(t, uint64(2), svc.Capture().Generation())
257
258 t.Run("cmd", func(t *testing.T) {
259 if runtime.GOOS == "windows" {
260 t.Skip("skipping on windows")
261 }
262
263 cmdCfg := testSingleVaultConfig()
264 cmdCfg["mode"] = "${cmd:/bin/echo token}"
265 require.NoError(t, svc.Update(context.Background(), storeKey, newStoreFromConfig(t, svc, secretstore.KindVault, cmdCfg)))
266 assert.Equal(t, uint64(3), svc.Capture().Generation())
267 })
268 }
269
270 func TestServiceValidate_RejectsStoreRefsInProviderPayload(t *testing.T) {
271 svc := secretstore.NewService(backends.Creators()...)
272
273 cfg := testSingleVaultConfig()
274 cfg["mode"] = "${store:vault:vault_prod:value}"
275
276 err := svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, cfg))
277 require.Error(t, err)
278 assert.ErrorContains(t, err, "secretstore resolver is not configured")
279 }
280
281 func TestServiceValidate_KeepsMetadataStatic(t *testing.T) {
282 t.Setenv("TEST_STORE_NAME", "vault_prod")
283
284 svc := secretstore.NewService(backends.Creators()...)
285
286 cfg := testSingleVaultConfig()
287 cfg["name"] = "${env:TEST_STORE_NAME}"
288
289 err := svc.Validate(context.Background(), newStoreFromConfig(t, svc, secretstore.KindVault, cfg))
290 require.Error(t, err)
291 assert.ErrorContains(t, err, "invalid store name")
292 }
293
294 func TestServiceValidate_LogsBuiltinResolutionWithContext(t *testing.T) {
295 t.Setenv("TEST_VAULT_MODE", "token")
296
297 svc := secretstore.NewService(backends.Creators()...)
298 cfg := testSingleVaultConfig()
299 cfg["mode"] = "${env:TEST_VAULT_MODE}"
300
301 out := captureLoggerOutput(t, func(log *logger.Logger) {
302 ctx := logger.ContextWithLogger(context.Background(), log)
303 require.NoError(t, svc.Validate(ctx, newStoreFromConfig(t, svc, secretstore.KindVault, cfg)))
304 })
305
306 assert.Contains(t, out, "resolved secret via env variable 'TEST_VAULT_MODE'")
307 assert.NotContains(t, out, "token")
308 }