validation_test.go 12 KB

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