auth_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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 vault
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "testing"
  19. "github.com/google/go-cmp/cmp"
  20. "github.com/google/go-cmp/cmp/cmpopts"
  21. vault "github.com/hashicorp/vault/api"
  22. corev1 "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/utils/ptr"
  25. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  28. "github.com/external-secrets/external-secrets/providers/v1/vault/fake"
  29. )
  30. // Test Vault Namespace logic.
  31. func TestSetAuthNamespace(t *testing.T) {
  32. store := makeValidSecretStore()
  33. kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
  34. ObjectMeta: metav1.ObjectMeta{
  35. Name: "vault-secret",
  36. Namespace: "default",
  37. },
  38. Data: map[string][]byte{
  39. "key": []byte("token"),
  40. },
  41. }).Build()
  42. store.Spec.Provider.Vault.Auth.Kubernetes.ServiceAccountRef = nil
  43. store.Spec.Provider.Vault.Auth.Kubernetes.SecretRef = &esmeta.SecretKeySelector{
  44. Name: "vault-secret",
  45. Namespace: ptr.To("default"),
  46. Key: "key",
  47. }
  48. adminNS := "admin"
  49. teamNS := "admin/team-a"
  50. type result struct {
  51. Before string
  52. During string
  53. After string
  54. }
  55. type args struct {
  56. store *esv1.SecretStore
  57. expected result
  58. }
  59. cases := map[string]struct {
  60. reason string
  61. args args
  62. }{
  63. "StoreNoNamespace": {
  64. reason: "no namespace should ever be set",
  65. args: args{
  66. store: store,
  67. expected: result{Before: "", During: "", After: ""},
  68. },
  69. },
  70. "StoreWithNamespace": {
  71. reason: "use the team namespace throughout",
  72. args: args{
  73. store: func(store *esv1.SecretStore) *esv1.SecretStore {
  74. s := store.DeepCopy()
  75. s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
  76. return s
  77. }(store),
  78. expected: result{Before: teamNS, During: teamNS, After: teamNS},
  79. },
  80. },
  81. "StoreWithAuthNamespace": {
  82. reason: "switch to the auth namespace during login then revert",
  83. args: args{
  84. store: func(store *esv1.SecretStore) *esv1.SecretStore {
  85. s := store.DeepCopy()
  86. s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
  87. return s
  88. }(store),
  89. expected: result{Before: "", During: adminNS, After: ""},
  90. },
  91. },
  92. "StoreWithSameNamespace": {
  93. reason: "the admin namespace throughout",
  94. args: args{
  95. store: func(store *esv1.SecretStore) *esv1.SecretStore {
  96. s := store.DeepCopy()
  97. s.Spec.Provider.Vault.Namespace = ptr.To(adminNS)
  98. s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
  99. return s
  100. }(store),
  101. expected: result{Before: adminNS, During: adminNS, After: adminNS},
  102. },
  103. },
  104. "StoreWithDistinctNamespace": {
  105. reason: "switch from team namespace, to admin, then back",
  106. args: args{
  107. store: func(store *esv1.SecretStore) *esv1.SecretStore {
  108. s := store.DeepCopy()
  109. s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
  110. s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
  111. return s
  112. }(store),
  113. expected: result{Before: teamNS, During: adminNS, After: teamNS},
  114. },
  115. },
  116. }
  117. for name, tc := range cases {
  118. t.Run(name, func(t *testing.T) {
  119. prov := &Provider{
  120. NewVaultClient: fake.ClientWithLoginMock,
  121. }
  122. c, cfg, err := prov.prepareConfig(context.Background(), kube, nil, tc.args.store.Spec.Provider.Vault, nil, "default", store.GetObjectKind().GroupVersionKind().Kind)
  123. if err != nil {
  124. t.Error(err.Error())
  125. }
  126. client, err := getVaultClient(prov, tc.args.store, cfg, "default")
  127. if err != nil {
  128. t.Errorf("vault.useAuthNamespace: failed to create client: %s", err.Error())
  129. }
  130. _, err = prov.initClient(context.Background(), c, client, cfg, tc.args.store.Spec.Provider.Vault)
  131. if err != nil {
  132. t.Errorf("vault.useAuthNamespace: failed to init client: %s", err.Error())
  133. }
  134. c.client = client
  135. // before auth
  136. actual := result{
  137. Before: c.client.Namespace(),
  138. }
  139. // during authentication (getting a token)
  140. resetNS := c.useAuthNamespace(context.Background())
  141. actual.During = c.client.Namespace()
  142. resetNS()
  143. // after getting the token
  144. actual.After = c.client.Namespace()
  145. if diff := cmp.Diff(tc.args.expected, actual, cmpopts.EquateComparable()); diff != "" {
  146. t.Errorf("\n%s\nvault.useAuthNamepsace(...): -want namespace, +got namespace:\n%s", tc.reason, diff)
  147. }
  148. })
  149. }
  150. }
  151. func TestCheckTokenErrors(t *testing.T) {
  152. cases := map[string]struct {
  153. message string
  154. secret *vault.Secret
  155. err error
  156. }{
  157. "SuccessWithNoData": {
  158. message: "should not cache if token lookup returned no data",
  159. secret: &vault.Secret{},
  160. err: nil,
  161. },
  162. "Error": {
  163. message: "should not cache if token lookup errored",
  164. secret: nil,
  165. err: errors.New(""),
  166. },
  167. // This happens when a token is expired and the Vault server returns:
  168. // {"errors":["permission denied"]}
  169. "NoDataNorError": {
  170. message: "should not cache if token lookup returned no data nor error",
  171. secret: nil,
  172. err: nil,
  173. },
  174. }
  175. for name, tc := range cases {
  176. t.Run(name, func(t *testing.T) {
  177. token := fake.Token{
  178. LookupSelfWithContextFn: func(_ context.Context) (*vault.Secret, error) {
  179. return tc.secret, tc.err
  180. },
  181. }
  182. cached, _ := checkToken(context.Background(), token)
  183. if cached {
  184. t.Errorf("%v", tc.message)
  185. }
  186. })
  187. }
  188. }
  189. func TestCheckTokenTtl(t *testing.T) {
  190. cases := map[string]struct {
  191. message string
  192. secret *vault.Secret
  193. cache bool
  194. }{
  195. "LongTTLExpirable": {
  196. message: "should cache if expirable token expires far into the future",
  197. secret: &vault.Secret{
  198. Data: map[string]interface{}{
  199. "expire_time": "2024-01-01T00:00:00.000000000Z",
  200. "ttl": json.Number("3600"),
  201. "type": "service",
  202. },
  203. },
  204. cache: true,
  205. },
  206. "ShortTTLExpirable": {
  207. message: "should not cache if expirable token is about to expire",
  208. secret: &vault.Secret{
  209. Data: map[string]interface{}{
  210. "expire_time": "2024-01-01T00:00:00.000000000Z",
  211. "ttl": json.Number("5"),
  212. "type": "service",
  213. },
  214. },
  215. cache: false,
  216. },
  217. "ZeroTTLExpirable": {
  218. message: "should not cache if expirable token has TTL of 0",
  219. secret: &vault.Secret{
  220. Data: map[string]interface{}{
  221. "expire_time": "2024-01-01T00:00:00.000000000Z",
  222. "ttl": json.Number("0"),
  223. "type": "service",
  224. },
  225. },
  226. cache: false,
  227. },
  228. "NonExpirable": {
  229. message: "should cache if token is non-expirable",
  230. secret: &vault.Secret{
  231. Data: map[string]interface{}{
  232. "expire_time": nil,
  233. "ttl": json.Number("0"),
  234. "type": "service",
  235. },
  236. },
  237. cache: true,
  238. },
  239. }
  240. for name, tc := range cases {
  241. t.Run(name, func(t *testing.T) {
  242. token := fake.Token{
  243. LookupSelfWithContextFn: func(_ context.Context) (*vault.Secret, error) {
  244. return tc.secret, nil
  245. },
  246. }
  247. cached, err := checkToken(context.Background(), token)
  248. if cached != tc.cache || err != nil {
  249. t.Errorf("%v: err = %v", tc.message, err)
  250. }
  251. })
  252. }
  253. }
  254. // Test GCP authentication detection logic.
  255. func TestGCPAuthDetection(t *testing.T) {
  256. tests := []struct {
  257. name string
  258. gcpAuth *esv1.VaultGCPAuth
  259. expectedHasAuth bool
  260. expectError bool
  261. }{
  262. {
  263. name: "GCP auth configured",
  264. gcpAuth: &esv1.VaultGCPAuth{
  265. Role: "test-role",
  266. Path: "gcp",
  267. },
  268. expectedHasAuth: true,
  269. expectError: true, // Will error because auth client is not initialized in test
  270. },
  271. {
  272. name: "No GCP auth configured",
  273. gcpAuth: nil,
  274. expectedHasAuth: false,
  275. expectError: false,
  276. },
  277. }
  278. for _, tt := range tests {
  279. t.Run(tt.name, func(t *testing.T) {
  280. // Create a mock client
  281. c := &client{
  282. store: &esv1.VaultProvider{
  283. Auth: &esv1.VaultAuth{
  284. GCP: tt.gcpAuth,
  285. },
  286. },
  287. // auth: nil (not initialized for test)
  288. }
  289. // Test detection logic
  290. hasAuth, err := setGcpAuthToken(context.Background(), c)
  291. if hasAuth != tt.expectedHasAuth {
  292. t.Errorf("setGcpAuthToken() returned hasAuth = %v, want %v", hasAuth, tt.expectedHasAuth)
  293. }
  294. if tt.expectError && err == nil {
  295. t.Errorf("setGcpAuthToken() expected error, got nil")
  296. }
  297. if !tt.expectError && err != nil {
  298. t.Errorf("setGcpAuthToken() unexpected error: %v", err)
  299. }
  300. })
  301. }
  302. }
  303. func TestGCPAuthMountPathDefault(t *testing.T) {
  304. c := &client{}
  305. tests := []struct {
  306. name string
  307. path string
  308. expected string
  309. }{
  310. {
  311. name: "default path when empty",
  312. path: "",
  313. expected: "gcp",
  314. },
  315. {
  316. name: "custom path",
  317. path: "custom-gcp",
  318. expected: "custom-gcp",
  319. },
  320. }
  321. for _, tt := range tests {
  322. t.Run(tt.name, func(t *testing.T) {
  323. result := c.getGCPAuthMountPathOrDefault(tt.path)
  324. if result != tt.expected {
  325. t.Errorf("getGCPAuthMountPathOrDefault() = %v, want %v", result, tt.expected)
  326. }
  327. })
  328. }
  329. }