validation_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 mysterybox
  14. import (
  15. "strings"
  16. "testing"
  17. tassert "github.com/stretchr/testify/assert"
  18. pointer "k8s.io/utils/ptr"
  19. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  20. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  21. )
  22. const (
  23. utilsErrNamespaceNotAllowed = "namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"
  24. utilsErrRequireNamespace = "cluster scope requires namespace"
  25. otherNs = "otherns"
  26. )
  27. func TestValidateStore(t *testing.T) {
  28. t.Parallel()
  29. p := &Provider{}
  30. mkStore := func(cfg func(*esv1.SecretStore)) esv1.GenericStore {
  31. st := &esv1.SecretStore{}
  32. st.Namespace = "test-ns"
  33. st.Spec.Provider = &esv1.SecretStoreProvider{NebiusMysterybox: &esv1.NebiusMysteryboxProvider{APIDomain: "api.public"}}
  34. if cfg != nil {
  35. cfg(st)
  36. }
  37. return st
  38. }
  39. tests := []struct {
  40. name string
  41. store esv1.GenericStore
  42. wantErr string
  43. }{
  44. {
  45. name: "nil store",
  46. store: nil,
  47. wantErr: errNilStore,
  48. },
  49. {
  50. name: "missing provider",
  51. store: mkStore(func(s *esv1.SecretStore) { s.Spec.Provider = nil }),
  52. wantErr: errMissingProvider,
  53. },
  54. {
  55. name: "missing nebius provider",
  56. store: &esv1.SecretStore{Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{}}},
  57. wantErr: "invalid provider spec.",
  58. },
  59. {
  60. name: "invalid auth: none provided",
  61. store: mkStore(func(s *esv1.SecretStore) {}),
  62. wantErr: errMissingAuthOptions,
  63. },
  64. {
  65. name: "invalid auth: both provided",
  66. store: mkStore(func(s *esv1.SecretStore) {
  67. nm := s.Spec.Provider.NebiusMysterybox
  68. nm.Auth.Token = esmeta.SecretKeySelector{Name: "a", Key: "k"}
  69. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "b", Key: "k"}
  70. }),
  71. wantErr: errInvalidAuthConfig,
  72. },
  73. {
  74. name: "invalid token auth: missing key",
  75. store: mkStore(func(s *esv1.SecretStore) {
  76. nm := s.Spec.Provider.NebiusMysterybox
  77. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok"}
  78. }),
  79. wantErr: errInvalidTokenAuthConfig,
  80. },
  81. {
  82. name: "invalid token auth: missing name",
  83. store: mkStore(func(s *esv1.SecretStore) {
  84. nm := s.Spec.Provider.NebiusMysterybox
  85. nm.Auth.Token = esmeta.SecretKeySelector{Key: "key"}
  86. }),
  87. wantErr: errMissingAuthOptions,
  88. },
  89. {
  90. name: "invalid sa creds auth: missing key",
  91. store: mkStore(func(s *esv1.SecretStore) {
  92. nm := s.Spec.Provider.NebiusMysterybox
  93. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "creds"}
  94. }),
  95. wantErr: errInvalidSACredsAuthConfig,
  96. },
  97. {
  98. name: "invalid sa creds auth: missing name",
  99. store: mkStore(func(s *esv1.SecretStore) {
  100. nm := s.Spec.Provider.NebiusMysterybox
  101. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Key: "key"}
  102. }),
  103. wantErr: errMissingAuthOptions,
  104. },
  105. {
  106. name: "valid: token auth",
  107. store: mkStore(func(s *esv1.SecretStore) {
  108. nm := s.Spec.Provider.NebiusMysterybox
  109. nm.APIDomain = apiDomain
  110. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  111. }),
  112. },
  113. {
  114. name: "valid: service account creds",
  115. store: mkStore(func(s *esv1.SecretStore) {
  116. nm := s.Spec.Provider.NebiusMysterybox
  117. nm.APIDomain = apiDomain
  118. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "creds", Key: "k"}
  119. }),
  120. },
  121. {
  122. name: "missing apiDomain",
  123. store: mkStore(func(s *esv1.SecretStore) { s.Spec.Provider.NebiusMysterybox.APIDomain = "" }),
  124. wantErr: errMissingAPIDomain,
  125. },
  126. {
  127. name: "token selector different namespace (namespaced store)",
  128. store: mkStore(func(s *esv1.SecretStore) {
  129. ns := otherNs
  130. nm := s.Spec.Provider.NebiusMysterybox
  131. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: &ns}
  132. }),
  133. wantErr: utilsErrNamespaceNotAllowed,
  134. },
  135. {
  136. name: "sa creds selector different namespace (namespaced store)",
  137. store: mkStore(func(s *esv1.SecretStore) {
  138. ns := otherNs
  139. nm := s.Spec.Provider.NebiusMysterybox
  140. nm.Auth.Token = esmeta.SecretKeySelector{}
  141. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "creds", Key: "k", Namespace: &ns}
  142. }),
  143. wantErr: utilsErrNamespaceNotAllowed,
  144. },
  145. {
  146. name: "ca cert specified without secret name",
  147. store: mkStore(func(s *esv1.SecretStore) {
  148. ns := otherNs
  149. nm := s.Spec.Provider.NebiusMysterybox
  150. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  151. nm.CAProvider = &esv1.NebiusCAProvider{Certificate: esmeta.SecretKeySelector{Namespace: &ns}}
  152. }),
  153. wantErr: errInvalidCertificateConfigNoNameSpecified,
  154. },
  155. {
  156. name: "ca cert specified without secret key",
  157. store: mkStore(func(s *esv1.SecretStore) {
  158. ns := otherNs
  159. nm := s.Spec.Provider.NebiusMysterybox
  160. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  161. nm.CAProvider = &esv1.NebiusCAProvider{Certificate: esmeta.SecretKeySelector{Name: "cacert", Namespace: &ns}}
  162. }),
  163. wantErr: errInvalidCertificateConfigNoKeySpecified,
  164. },
  165. {
  166. name: "ca cert selector different namespace (namespaced store)",
  167. store: mkStore(func(s *esv1.SecretStore) {
  168. ns := otherNs
  169. nm := s.Spec.Provider.NebiusMysterybox
  170. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  171. nm.CAProvider = &esv1.NebiusCAProvider{Certificate: esmeta.SecretKeySelector{Name: "ca", Key: "tls.crt", Namespace: &ns}}
  172. }),
  173. wantErr: utilsErrNamespaceNotAllowed,
  174. },
  175. {
  176. name: "matching selector namespace passes",
  177. store: mkStore(func(s *esv1.SecretStore) {
  178. ns := s.Namespace
  179. nm := s.Spec.Provider.NebiusMysterybox
  180. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: &ns}
  181. }),
  182. wantErr: "",
  183. },
  184. }
  185. for _, tt := range tests {
  186. t.Run(tt.name, func(t *testing.T) {
  187. t.Parallel()
  188. _, err := p.ValidateStore(tt.store)
  189. if tt.wantErr == "" {
  190. tassert.NoError(t, err, "%s: unexpected error", tt.name)
  191. return
  192. }
  193. tassert.NotNil(t, err, "%s: expected error containing %q, got nil", tt.name, tt.wantErr)
  194. if err != nil {
  195. tassert.Contains(t, err.Error(), tt.wantErr, "%s: error %q does not contain %q", tt.name, err.Error(), tt.wantErr)
  196. }
  197. })
  198. }
  199. }
  200. func TestValidateStoreClusterScope(t *testing.T) {
  201. t.Parallel()
  202. p := &Provider{}
  203. makeStore := func(cfg func(*esv1.NebiusMysteryboxProvider)) esv1.GenericStore {
  204. css := &esv1.ClusterSecretStore{}
  205. css.TypeMeta.Kind = esv1.ClusterSecretStoreKind
  206. nm := &esv1.NebiusMysteryboxProvider{APIDomain: "api.public"}
  207. if cfg != nil {
  208. cfg(nm)
  209. }
  210. css.Spec.Provider = &esv1.SecretStoreProvider{NebiusMysterybox: nm}
  211. return css
  212. }
  213. tests := []struct {
  214. name string
  215. store esv1.GenericStore
  216. wantErr string
  217. }{
  218. {
  219. name: "cluster: token selector requires namespace",
  220. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  221. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  222. }),
  223. wantErr: utilsErrRequireNamespace,
  224. },
  225. {
  226. name: "cluster: namespaced token passes",
  227. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  228. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: pointer.To("ns1")}
  229. }),
  230. wantErr: "",
  231. },
  232. {
  233. name: "cluster: sa creds selector requires namespace",
  234. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  235. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  236. }),
  237. wantErr: utilsErrRequireNamespace,
  238. },
  239. {
  240. name: "cluster: namespaced sa creds passes",
  241. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  242. nm.Auth.ServiceAccountCreds = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: pointer.To("ns1")}
  243. }),
  244. wantErr: "",
  245. },
  246. {
  247. name: "cluster: ca cert requires namespace",
  248. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  249. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: pointer.To("ns1")}
  250. nm.CAProvider = &esv1.NebiusCAProvider{Certificate: esmeta.SecretKeySelector{Name: "ca", Key: "tls.crt"}}
  251. }),
  252. wantErr: utilsErrRequireNamespace,
  253. },
  254. {
  255. name: "cluster: namespaced ca cert passes",
  256. store: makeStore(func(nm *esv1.NebiusMysteryboxProvider) {
  257. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k", Namespace: pointer.To("ns1")}
  258. nm.CAProvider = &esv1.NebiusCAProvider{Certificate: esmeta.SecretKeySelector{Name: "ca", Key: "tls.crt", Namespace: pointer.To("ns1")}}
  259. }),
  260. wantErr: "",
  261. },
  262. }
  263. for _, tt := range tests {
  264. t.Run(tt.name, func(t *testing.T) {
  265. t.Parallel()
  266. _, err := p.ValidateStore(tt.store)
  267. if tt.wantErr == "" {
  268. tassert.NoError(t, err, "%s: unexpected error", tt.name)
  269. return
  270. }
  271. if err == nil {
  272. tassert.Failf(t, "%s: expected error containing %q, got nil", tt.name, tt.wantErr)
  273. } else {
  274. tassert.Contains(t, err.Error(), tt.wantErr, "%s: expected error to contain substring", tt.name)
  275. }
  276. })
  277. }
  278. }
  279. func TestValidateStore_APIDomainCases(t *testing.T) {
  280. t.Parallel()
  281. p := &Provider{}
  282. mkStore := func(domain string) esv1.GenericStore {
  283. st := &esv1.SecretStore{}
  284. st.Namespace = "test-ns"
  285. st.Spec.Provider = &esv1.SecretStoreProvider{NebiusMysterybox: &esv1.NebiusMysteryboxProvider{APIDomain: domain}}
  286. nm := st.Spec.Provider.NebiusMysterybox
  287. nm.Auth.Token = esmeta.SecretKeySelector{Name: "tok", Key: "k"}
  288. return st
  289. }
  290. cases := []struct { //nolint:prealloc // struct literal with dynamic appends below
  291. name string
  292. domain string
  293. valid bool
  294. }{
  295. {name: "simple domain with port", domain: "example.com:443", valid: true},
  296. {name: "simple domain without port", domain: "example.com", valid: true},
  297. {name: "subdomain", domain: "sub.example.com", valid: true},
  298. {name: "hyphen in middle", domain: "a-b.com", valid: true},
  299. {name: "uppercase allowed", domain: "EXAMPLE.COM", valid: true},
  300. {name: "single label not allowed", domain: "com", valid: false},
  301. {name: "empty label (double dot)", domain: "a..com", valid: false},
  302. {name: "leading dot", domain: ".example.com", valid: false},
  303. {name: "trailing dot", domain: "example.com.", valid: false},
  304. {name: "label starts with hyphen", domain: "-abc.com", valid: false},
  305. {name: "label ends with hyphen", domain: "abc-.com", valid: false},
  306. {name: "invalid char underscore", domain: "ab_c.com", valid: false},
  307. {name: "invalid char space", domain: "exa mple.com", valid: false},
  308. {name: "numeric TLD not allowed", domain: "example.123", valid: false},
  309. {name: "ip address not a domain", domain: "127.0.0.1", valid: false},
  310. }
  311. longLabel := strings.Repeat("a", 64) + ".com"
  312. cases = append(cases, struct {
  313. name string
  314. domain string
  315. valid bool
  316. }{name: "label too long", domain: longLabel, valid: false})
  317. manyLabels := strings.Repeat("a.", 127) + "a"
  318. cases = append(cases, struct {
  319. name string
  320. domain string
  321. valid bool
  322. }{name: "domain too long", domain: manyLabels, valid: false})
  323. for _, tc := range cases {
  324. t.Run(tc.name, func(t *testing.T) {
  325. t.Parallel()
  326. store := mkStore(tc.domain)
  327. _, err := p.ValidateStore(store)
  328. if tc.valid {
  329. tassert.NoError(t, err, "%s: expected valid, got error", tc.name)
  330. } else {
  331. tassert.Error(t, err, "%s: expected error for domain %q", tc.name, tc.domain)
  332. }
  333. if err != nil {
  334. tassert.Contains(t, err.Error(), errInvalidAPIDomain, "%s: error should contain invalid api domain", tc.name)
  335. }
  336. })
  337. }
  338. }