passbolt_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package passbolt
  14. import (
  15. "context"
  16. "crypto/ecdsa"
  17. "crypto/elliptic"
  18. "crypto/rand"
  19. "crypto/tls"
  20. "crypto/x509"
  21. "crypto/x509/pkix"
  22. "encoding/pem"
  23. "errors"
  24. "math/big"
  25. "net/http"
  26. "testing"
  27. "time"
  28. g "github.com/onsi/gomega"
  29. "github.com/passbolt/go-passbolt/helper"
  30. corev1 "k8s.io/api/core/v1"
  31. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  32. "k8s.io/apimachinery/pkg/runtime"
  33. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  34. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  35. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  36. "github.com/external-secrets/external-secrets/runtime/esutils"
  37. )
  38. func TestValidateStore(t *testing.T) {
  39. p := &ProviderPassbolt{}
  40. g.RegisterTestingT(t)
  41. store := &esv1.SecretStore{
  42. Spec: esv1.SecretStoreSpec{
  43. Provider: &esv1.SecretStoreProvider{
  44. Passbolt: &esv1.PassboltProvider{},
  45. },
  46. },
  47. }
  48. // missing auth
  49. _, err := p.ValidateStore(store)
  50. g.Expect(err).To(g.BeEquivalentTo(errors.New(errPassboltStoreMissingAuth)))
  51. // missing password
  52. store.Spec.Provider.Passbolt.Auth = &esv1.PassboltAuth{
  53. PrivateKeySecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "privatekey"},
  54. }
  55. _, err = p.ValidateStore(store)
  56. g.Expect(err).To(g.BeEquivalentTo(errors.New(errPassboltStoreMissingAuthPassword)))
  57. // missing privateKey
  58. store.Spec.Provider.Passbolt.Auth = &esv1.PassboltAuth{
  59. PasswordSecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "password"},
  60. }
  61. _, err = p.ValidateStore(store)
  62. g.Expect(err).To(g.BeEquivalentTo(errors.New(errPassboltStoreMissingAuthPrivateKey)))
  63. store.Spec.Provider.Passbolt.Auth = &esv1.PassboltAuth{
  64. PasswordSecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "password"},
  65. PrivateKeySecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "privatekey"},
  66. }
  67. // missing host
  68. _, err = p.ValidateStore(store)
  69. g.Expect(err).To(g.BeEquivalentTo(errors.New(errPassboltStoreMissingHost)))
  70. // host not https
  71. store.Spec.Provider.Passbolt.Host = "http://passbolt.test"
  72. _, err = p.ValidateStore(store)
  73. g.Expect(err).To(g.BeEquivalentTo(errors.New(errPassboltStoreHostSchemeNotHTTPS)))
  74. // valid store
  75. store.Spec.Provider.Passbolt.Host = "https://passbolt.test"
  76. _, err = p.ValidateStore(store)
  77. g.Expect(err).To(g.BeNil())
  78. }
  79. func TestSecretGetProp(t *testing.T) {
  80. g.RegisterTestingT(t)
  81. secret := Secret{
  82. Name: "test-name",
  83. Username: "test-user",
  84. Password: "test-pass",
  85. URI: "https://test.com",
  86. Description: "test-desc",
  87. CustomFields: map[string]string{"my-field": "my-value"},
  88. }
  89. // Test valid properties
  90. val, err := secret.GetProp("name")
  91. g.Expect(err).To(g.BeNil())
  92. g.Expect(string(val)).To(g.Equal("test-name"))
  93. val, err = secret.GetProp("username")
  94. g.Expect(err).To(g.BeNil())
  95. g.Expect(string(val)).To(g.Equal("test-user"))
  96. val, err = secret.GetProp("password")
  97. g.Expect(err).To(g.BeNil())
  98. g.Expect(string(val)).To(g.Equal("test-pass"))
  99. val, err = secret.GetProp("uri")
  100. g.Expect(err).To(g.BeNil())
  101. g.Expect(string(val)).To(g.Equal("https://test.com"))
  102. val, err = secret.GetProp("description")
  103. g.Expect(err).To(g.BeNil())
  104. g.Expect(string(val)).To(g.Equal("test-desc"))
  105. // Test custom field
  106. val, err = secret.GetProp("custom_fields.my-field")
  107. g.Expect(err).To(g.BeNil())
  108. g.Expect(string(val)).To(g.Equal("my-value"))
  109. // Test invalid property
  110. _, err = secret.GetProp("invalid")
  111. g.Expect(err).To(g.MatchError(errPassboltSecretPropertyInvalid))
  112. }
  113. func TestSecretGetPropCustomFieldNotFound(t *testing.T) {
  114. g.RegisterTestingT(t)
  115. // No custom fields set at all.
  116. secret := Secret{Name: "test-name"}
  117. _, err := secret.GetProp("custom_fields.missing")
  118. g.Expect(err).To(g.MatchError(errPassboltCustomFieldNotFound))
  119. g.Expect(err).To(g.MatchError(g.ContainSubstring("missing")))
  120. // Custom fields present but the requested key does not exist.
  121. secret.CustomFields = map[string]string{"other-key": "v"}
  122. _, err = secret.GetProp("custom_fields.nonexistent")
  123. g.Expect(err).To(g.MatchError(errPassboltCustomFieldNotFound))
  124. g.Expect(err).To(g.MatchError(g.ContainSubstring("nonexistent")))
  125. }
  126. // The metadata/secret custom-field merge lives in the SDK
  127. // (helper.ParseCustomFields). This is a contract test over the shapes this
  128. // provider depends on, not a re-test of the library's full table.
  129. func TestCustomFieldsFromResourceFieldMaps(t *testing.T) {
  130. g.RegisterTestingT(t)
  131. const idA = "11111111-1111-1111-1111-111111111111"
  132. const idB = "22222222-2222-2222-2222-222222222222"
  133. tests := []struct {
  134. name string
  135. metaFields map[string]any
  136. secretFields map[string]any
  137. want map[string]string
  138. }{
  139. {
  140. name: "a resource with no custom fields yields no entries",
  141. metaFields: map[string]any{"name": "x"},
  142. secretFields: map[string]any{"password": "p"},
  143. want: map[string]string{},
  144. },
  145. {
  146. name: "encrypted value: name from metadata_key, value from secret_value",
  147. metaFields: map[string]any{
  148. "custom_fields": []any{map[string]any{"id": idA, "metadata_key": "api-key"}},
  149. },
  150. secretFields: map[string]any{
  151. "custom_fields": []any{map[string]any{"id": idA, "secret_value": "secret-123"}},
  152. },
  153. want: map[string]string{"api-key": "secret-123"},
  154. },
  155. {
  156. // A cleartext field still carries an empty secret_value, so reading
  157. // it requires preferring the first non-empty of the two sides.
  158. name: "cleartext value comes from metadata_value",
  159. metaFields: map[string]any{
  160. "custom_fields": []any{
  161. map[string]any{"id": idA, "metadata_key": "env", "metadata_value": "production"},
  162. },
  163. },
  164. secretFields: map[string]any{
  165. "custom_fields": []any{map[string]any{"id": idA, "secret_value": ""}},
  166. },
  167. want: map[string]string{"env": "production"},
  168. },
  169. {
  170. name: "a field whose name is encrypted is keyed by its decrypted secret_key",
  171. metaFields: map[string]any{
  172. "custom_fields": []any{map[string]any{"id": idA, "metadata_key": ""}},
  173. },
  174. secretFields: map[string]any{
  175. "custom_fields": []any{
  176. map[string]any{"id": idA, "secret_key": "hidden-name", "secret_value": "hidden-val"},
  177. },
  178. },
  179. want: map[string]string{"hidden-name": "hidden-val"},
  180. },
  181. {
  182. name: "non-string values are stringified for the map[string]string payload",
  183. metaFields: map[string]any{
  184. "custom_fields": []any{
  185. map[string]any{"id": idA, "metadata_key": "port"},
  186. map[string]any{"id": idB, "metadata_key": "enabled", "metadata_value": true},
  187. },
  188. },
  189. secretFields: map[string]any{
  190. "custom_fields": []any{
  191. map[string]any{"id": idA, "secret_value": float64(8080)},
  192. map[string]any{"id": idB, "secret_value": ""},
  193. },
  194. },
  195. want: map[string]string{"port": "8080", "enabled": "true"},
  196. },
  197. }
  198. for _, tt := range tests {
  199. t.Run(tt.name, func(t *testing.T) {
  200. got := helper.ParseCustomFields(tt.metaFields, tt.secretFields).Map()
  201. g.Expect(got).To(g.Equal(tt.want))
  202. })
  203. }
  204. }
  205. // A resource without custom fields must not grow a "custom_fields" key in the
  206. // JSON payload, whether the map comes back empty or nil.
  207. func TestSecretJSONOmitsEmptyCustomFields(t *testing.T) {
  208. g.RegisterTestingT(t)
  209. for _, cf := range []map[string]string{nil, {}} {
  210. marshaled, err := esutils.JSONMarshal(Secret{Name: "x", CustomFields: cf})
  211. g.Expect(err).ToNot(g.HaveOccurred())
  212. g.Expect(string(marshaled)).ToNot(g.ContainSubstring("custom_fields"))
  213. }
  214. }
  215. func TestCapabilities(t *testing.T) {
  216. g.RegisterTestingT(t)
  217. p := &ProviderPassbolt{}
  218. g.Expect(p.Capabilities()).To(g.Equal(esv1.SecretStoreReadOnly))
  219. }
  220. func TestSecretExists(t *testing.T) {
  221. p := &ProviderPassbolt{}
  222. g.RegisterTestingT(t)
  223. _, err := p.SecretExists(context.TODO(), nil)
  224. g.Expect(err).To(g.BeEquivalentTo(errors.New(errNotImplemented)))
  225. }
  226. func TestPushSecret(t *testing.T) {
  227. p := &ProviderPassbolt{}
  228. g.RegisterTestingT(t)
  229. err := p.PushSecret(context.TODO(), nil, nil)
  230. g.Expect(err).To(g.BeEquivalentTo(errors.New(errNotImplemented)))
  231. }
  232. func TestDeleteSecret(t *testing.T) {
  233. p := &ProviderPassbolt{}
  234. g.RegisterTestingT(t)
  235. err := p.DeleteSecret(context.TODO(), nil)
  236. g.Expect(err).To(g.BeEquivalentTo(errors.New(errNotImplemented)))
  237. }
  238. func TestGetSecretMap(t *testing.T) {
  239. p := &ProviderPassbolt{}
  240. g.RegisterTestingT(t)
  241. _, err := p.GetSecretMap(context.TODO(), esv1.ExternalSecretDataRemoteRef{})
  242. g.Expect(err).To(g.BeEquivalentTo(errors.New(errNotImplemented)))
  243. }
  244. // generateCABundlePEM creates a self-signed CA certificate in PEM format,
  245. // for exercising buildHTTPClient without any real PKI dependencies.
  246. func generateCABundlePEM(t *testing.T) []byte {
  247. t.Helper()
  248. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  249. if err != nil {
  250. t.Fatalf("ecdsa key: %v", err)
  251. }
  252. tmpl := &x509.Certificate{
  253. SerialNumber: big.NewInt(1),
  254. Subject: pkix.Name{CommonName: "test"},
  255. NotBefore: time.Now().Add(-time.Hour),
  256. NotAfter: time.Now().Add(time.Hour),
  257. KeyUsage: x509.KeyUsageCertSign,
  258. BasicConstraintsValid: true,
  259. IsCA: true,
  260. }
  261. der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
  262. if err != nil {
  263. t.Fatalf("create cert: %v", err)
  264. }
  265. return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
  266. }
  267. func TestBuildHTTPClient(t *testing.T) {
  268. g.RegisterTestingT(t)
  269. caPEM := generateCABundlePEM(t)
  270. scheme := runtime.NewScheme()
  271. g.Expect(corev1.AddToScheme(scheme)).To(g.Succeed())
  272. kubeObjs := []runtime.Object{
  273. &corev1.Secret{
  274. ObjectMeta: metav1.ObjectMeta{Name: "passbolt-ca", Namespace: "ns"},
  275. Data: map[string][]byte{"ca.crt": caPEM},
  276. },
  277. }
  278. kube := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(kubeObjs...).Build()
  279. tests := []struct {
  280. name string
  281. provider *esv1.PassboltProvider
  282. wantNil bool
  283. }{
  284. {
  285. name: "no CA configured returns nil client (SDK default + system roots)",
  286. provider: &esv1.PassboltProvider{},
  287. wantNil: true,
  288. },
  289. {
  290. name: "inline caBundle populates the client's RootCAs and leaves default transport settings intact",
  291. provider: &esv1.PassboltProvider{
  292. CABundle: caPEM,
  293. },
  294. },
  295. {
  296. name: "caProvider with a Secret is dereferenced into RootCAs",
  297. provider: &esv1.PassboltProvider{
  298. CAProvider: &esv1.CAProvider{
  299. Type: esv1.CAProviderTypeSecret,
  300. Name: "passbolt-ca",
  301. Key: "ca.crt",
  302. },
  303. },
  304. },
  305. }
  306. for _, tt := range tests {
  307. t.Run(tt.name, func(t *testing.T) {
  308. client, err := buildHTTPClient(t.Context(), tt.provider, kube, esv1.SecretStoreKind, "ns")
  309. g.Expect(err).ToNot(g.HaveOccurred())
  310. if tt.wantNil {
  311. g.Expect(client).To(g.BeNil())
  312. return
  313. }
  314. g.Expect(client).ToNot(g.BeNil())
  315. transport, ok := client.Transport.(*http.Transport)
  316. g.Expect(ok).To(g.BeTrue())
  317. g.Expect(transport.TLSClientConfig).ToNot(g.BeNil())
  318. g.Expect(transport.TLSClientConfig.MinVersion).To(g.Equal(uint16(tls.VersionTLS12)))
  319. // Verify the configured pool actually contains *our* CA, not just any
  320. // non-nil pool — guards against silently loading a different bundle.
  321. expectedPool := x509.NewCertPool()
  322. g.Expect(expectedPool.AppendCertsFromPEM(caPEM)).To(g.BeTrue())
  323. g.Expect(transport.TLSClientConfig.RootCAs).ToNot(g.BeNil())
  324. g.Expect(transport.TLSClientConfig.RootCAs.Equal(expectedPool)).To(g.BeTrue())
  325. // Confirm we cloned the default transport and didn't end up with a
  326. // bare http.Transport{} (which would drop proxy/dialer defaults).
  327. defaultTransport := http.DefaultTransport.(*http.Transport)
  328. g.Expect(transport.Proxy).ToNot(g.BeNil(), "cloned transport should keep DefaultTransport.Proxy")
  329. g.Expect(transport.MaxIdleConns).To(g.Equal(defaultTransport.MaxIdleConns))
  330. })
  331. }
  332. }