keyvault_auth_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package keyvault
  13. import (
  14. "context"
  15. "net/http"
  16. "os"
  17. "strings"
  18. "testing"
  19. "github.com/Azure/go-autorest/autorest"
  20. "github.com/Azure/go-autorest/autorest/adal"
  21. tassert "github.com/stretchr/testify/assert"
  22. corev1 "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. pointer "k8s.io/utils/ptr"
  25. "sigs.k8s.io/controller-runtime/pkg/client"
  26. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  27. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  28. v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
  29. utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
  30. )
  31. var vaultURL = "https://local.vault.url"
  32. func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
  33. namespace := "internal"
  34. identityID := "1234"
  35. authType := esv1beta1.AzureManagedIdentity
  36. store := esv1beta1.SecretStore{
  37. ObjectMeta: metav1.ObjectMeta{
  38. Namespace: namespace,
  39. },
  40. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
  41. AuthType: &authType,
  42. IdentityID: &identityID,
  43. VaultURL: &vaultURL,
  44. }}},
  45. }
  46. k8sClient := clientfake.NewClientBuilder().Build()
  47. az := &Azure{
  48. crClient: k8sClient,
  49. namespace: namespace,
  50. provider: store.Spec.Provider.AzureKV,
  51. store: &store,
  52. }
  53. authorizer, err := az.authorizerForManagedIdentity()
  54. if err != nil {
  55. // On non Azure environment, MSI auth not available, so this error should be returned
  56. tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available")
  57. } else {
  58. // On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI
  59. tassert.NotNil(t, authorizer)
  60. }
  61. }
  62. func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
  63. const (
  64. tenantID = "my-tenant-id"
  65. clientID = "my-client-id"
  66. azAccessToken = "my-access-token"
  67. saToken = "FAKETOKEN"
  68. saName = "az-wi"
  69. namespace = "default"
  70. secretName = "mi-spec"
  71. )
  72. // create a temporary file to imitate
  73. // azure workload identity webhook
  74. // see AZURE_FEDERATED_TOKEN_FILE
  75. tf, err := os.CreateTemp("", "")
  76. tassert.Nil(t, err)
  77. defer os.RemoveAll(tf.Name())
  78. _, err = tf.WriteString(saToken)
  79. tassert.Nil(t, err)
  80. tokenFile := tf.Name()
  81. authType := esv1beta1.AzureWorkloadIdentity
  82. defaultProvider := &esv1beta1.AzureKVProvider{
  83. VaultURL: &vaultURL,
  84. AuthType: &authType,
  85. ServiceAccountRef: &v1.ServiceAccountSelector{
  86. Name: saName,
  87. },
  88. }
  89. type testCase struct {
  90. name string
  91. provider *esv1beta1.AzureKVProvider
  92. k8sObjects []client.Object
  93. prep func(*testing.T)
  94. expErr string
  95. }
  96. for _, row := range []testCase{
  97. {
  98. name: "missing service account",
  99. provider: defaultProvider,
  100. expErr: "serviceaccounts \"" + saName + "\" not found",
  101. },
  102. {
  103. name: "missing webhook env vars",
  104. provider: &esv1beta1.AzureKVProvider{},
  105. expErr: "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set",
  106. },
  107. {
  108. name: "missing workload identity token file",
  109. provider: &esv1beta1.AzureKVProvider{},
  110. prep: func(t *testing.T) {
  111. t.Setenv("AZURE_CLIENT_ID", clientID)
  112. t.Setenv("AZURE_TENANT_ID", tenantID)
  113. t.Setenv("AZURE_FEDERATED_TOKEN_FILE", "invalid file")
  114. },
  115. expErr: "unable to read token file invalid file: open invalid file: no such file or directory",
  116. },
  117. {
  118. name: "correct workload identity",
  119. provider: &esv1beta1.AzureKVProvider{},
  120. prep: func(t *testing.T) {
  121. t.Setenv("AZURE_CLIENT_ID", clientID)
  122. t.Setenv("AZURE_TENANT_ID", tenantID)
  123. t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
  124. },
  125. },
  126. {
  127. name: "missing sa annotations, tenantID, and clientId/tenantId AuthSecretRef",
  128. provider: defaultProvider,
  129. k8sObjects: []client.Object{
  130. &corev1.ServiceAccount{
  131. ObjectMeta: metav1.ObjectMeta{
  132. Name: saName,
  133. Namespace: namespace,
  134. Annotations: map[string]string{},
  135. },
  136. },
  137. },
  138. expErr: "missing clientID: either serviceAccountRef or service account annotation 'azure.workload.identity/client-id' is missing",
  139. },
  140. {
  141. name: "duplicated clientId",
  142. provider: &esv1beta1.AzureKVProvider{
  143. VaultURL: &vaultURL,
  144. AuthType: &authType,
  145. TenantID: pointer.To(tenantID),
  146. ServiceAccountRef: &v1.ServiceAccountSelector{
  147. Name: saName,
  148. },
  149. AuthSecretRef: &esv1beta1.AzureKVAuth{
  150. ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
  151. TenantID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: tenantID},
  152. },
  153. },
  154. k8sObjects: []client.Object{
  155. &corev1.ServiceAccount{
  156. ObjectMeta: metav1.ObjectMeta{
  157. Name: saName,
  158. Namespace: namespace,
  159. Annotations: map[string]string{
  160. AnnotationClientID: clientID,
  161. AnnotationTenantID: tenantID,
  162. },
  163. },
  164. },
  165. &corev1.Secret{
  166. ObjectMeta: metav1.ObjectMeta{
  167. Name: secretName,
  168. Namespace: namespace,
  169. },
  170. Data: map[string][]byte{
  171. clientID: []byte("clientid"),
  172. tenantID: []byte("tenantid"),
  173. },
  174. },
  175. },
  176. expErr: "multiple clientID found. Check secretRef and serviceAccountRef",
  177. },
  178. {
  179. name: "duplicated tenantId",
  180. provider: &esv1beta1.AzureKVProvider{
  181. VaultURL: &vaultURL,
  182. AuthType: &authType,
  183. TenantID: pointer.To(tenantID),
  184. ServiceAccountRef: &v1.ServiceAccountSelector{
  185. Name: saName,
  186. },
  187. },
  188. k8sObjects: []client.Object{
  189. &corev1.ServiceAccount{
  190. ObjectMeta: metav1.ObjectMeta{
  191. Name: saName,
  192. Namespace: namespace,
  193. Annotations: map[string]string{
  194. AnnotationClientID: clientID,
  195. AnnotationTenantID: tenantID,
  196. },
  197. },
  198. },
  199. },
  200. expErr: "multiple tenantID found. Check secretRef, 'spec.provider.azurekv.tenantId', and serviceAccountRef",
  201. },
  202. {
  203. name: "successful case #1: ClientID, TenantID from ServiceAccountRef",
  204. provider: defaultProvider,
  205. k8sObjects: []client.Object{
  206. &corev1.ServiceAccount{
  207. ObjectMeta: metav1.ObjectMeta{
  208. Name: saName,
  209. Namespace: namespace,
  210. Annotations: map[string]string{
  211. AnnotationClientID: clientID,
  212. AnnotationTenantID: tenantID,
  213. },
  214. },
  215. },
  216. },
  217. },
  218. {
  219. name: "successful case #2: ClientID, TenantID from AuthSecretRef",
  220. provider: &esv1beta1.AzureKVProvider{
  221. VaultURL: &vaultURL,
  222. AuthType: &authType,
  223. ServiceAccountRef: &v1.ServiceAccountSelector{
  224. Name: saName,
  225. },
  226. AuthSecretRef: &esv1beta1.AzureKVAuth{
  227. ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
  228. TenantID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: tenantID},
  229. },
  230. },
  231. k8sObjects: []client.Object{
  232. &corev1.ServiceAccount{
  233. ObjectMeta: metav1.ObjectMeta{
  234. Name: saName,
  235. Namespace: namespace,
  236. Annotations: map[string]string{},
  237. },
  238. },
  239. &corev1.Secret{
  240. ObjectMeta: metav1.ObjectMeta{
  241. Name: secretName,
  242. Namespace: namespace,
  243. },
  244. Data: map[string][]byte{
  245. clientID: []byte("clientid"),
  246. tenantID: []byte("tenantid"),
  247. },
  248. },
  249. },
  250. },
  251. {
  252. name: "successful case #3: ClientID from AuthSecretRef, TenantID from provider",
  253. provider: &esv1beta1.AzureKVProvider{
  254. VaultURL: &vaultURL,
  255. AuthType: &authType,
  256. TenantID: pointer.To(tenantID),
  257. ServiceAccountRef: &v1.ServiceAccountSelector{
  258. Name: saName,
  259. },
  260. AuthSecretRef: &esv1beta1.AzureKVAuth{
  261. ClientID: &v1.SecretKeySelector{Name: secretName, Namespace: pointer.To(namespace), Key: clientID},
  262. },
  263. },
  264. k8sObjects: []client.Object{
  265. &corev1.ServiceAccount{
  266. ObjectMeta: metav1.ObjectMeta{
  267. Name: saName,
  268. Namespace: namespace,
  269. Annotations: map[string]string{},
  270. },
  271. },
  272. &corev1.Secret{
  273. ObjectMeta: metav1.ObjectMeta{
  274. Name: secretName,
  275. Namespace: namespace,
  276. },
  277. Data: map[string][]byte{
  278. clientID: []byte("clientid"),
  279. },
  280. },
  281. },
  282. },
  283. } {
  284. t.Run(row.name, func(t *testing.T) {
  285. store := esv1beta1.SecretStore{
  286. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
  287. AzureKV: row.provider,
  288. }},
  289. }
  290. k8sClient := clientfake.NewClientBuilder().
  291. WithObjects(row.k8sObjects...).
  292. Build()
  293. az := &Azure{
  294. store: &store,
  295. namespace: namespace,
  296. crClient: k8sClient,
  297. kubeClient: utilfake.NewCreateTokenMock().WithToken(saToken),
  298. provider: store.Spec.Provider.AzureKV,
  299. }
  300. tokenProvider := func(ctx context.Context, token, clientID, tenantID, aadEndpoint, kvResource string) (adal.OAuthTokenProvider, error) {
  301. tassert.Equal(t, token, saToken)
  302. tassert.Equal(t, clientID, clientID)
  303. tassert.Equal(t, tenantID, tenantID)
  304. return &tokenProvider{accessToken: azAccessToken}, nil
  305. }
  306. if row.prep != nil {
  307. row.prep(t)
  308. }
  309. authorizer, err := az.authorizerForWorkloadIdentity(context.Background(), tokenProvider)
  310. if row.expErr == "" {
  311. tassert.NotNil(t, authorizer)
  312. tassert.Equal(t, getTokenFromAuthorizer(t, authorizer), azAccessToken)
  313. } else {
  314. tassert.EqualError(t, err, row.expErr)
  315. }
  316. })
  317. }
  318. }
  319. func TestAuth(t *testing.T) {
  320. defaultStore := esv1beta1.SecretStore{
  321. ObjectMeta: metav1.ObjectMeta{
  322. Namespace: "default",
  323. },
  324. Spec: esv1beta1.SecretStoreSpec{
  325. Provider: &esv1beta1.SecretStoreProvider{},
  326. },
  327. }
  328. authType := esv1beta1.AzureServicePrincipal
  329. type testCase struct {
  330. name string
  331. provider *esv1beta1.AzureKVProvider
  332. store esv1beta1.GenericStore
  333. objects []client.Object
  334. expErr string
  335. }
  336. for _, row := range []testCase{
  337. {
  338. name: "bad config",
  339. expErr: "missing secretRef in provider config",
  340. store: &defaultStore,
  341. provider: &esv1beta1.AzureKVProvider{
  342. AuthType: &authType,
  343. VaultURL: &vaultURL,
  344. TenantID: pointer.To("mytenant"),
  345. },
  346. },
  347. {
  348. name: "bad config",
  349. expErr: "missing accessKeyID/secretAccessKey in store config",
  350. store: &defaultStore,
  351. provider: &esv1beta1.AzureKVProvider{
  352. AuthType: &authType,
  353. VaultURL: &vaultURL,
  354. TenantID: pointer.To("mytenant"),
  355. AuthSecretRef: &esv1beta1.AzureKVAuth{},
  356. },
  357. },
  358. {
  359. name: "bad config: missing secret",
  360. expErr: "cannot get Kubernetes secret \"password\": secrets \"password\" not found",
  361. store: &defaultStore,
  362. provider: &esv1beta1.AzureKVProvider{
  363. AuthType: &authType,
  364. VaultURL: &vaultURL,
  365. TenantID: pointer.To("mytenant"),
  366. AuthSecretRef: &esv1beta1.AzureKVAuth{
  367. ClientSecret: &v1.SecretKeySelector{Name: "password"},
  368. ClientID: &v1.SecretKeySelector{Name: "password"},
  369. },
  370. },
  371. },
  372. {
  373. name: "cluster secret store",
  374. expErr: "cannot get Kubernetes secret \"password\": secrets \"password\" not found",
  375. store: &esv1beta1.ClusterSecretStore{
  376. TypeMeta: metav1.TypeMeta{
  377. Kind: esv1beta1.ClusterSecretStoreKind,
  378. },
  379. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
  380. },
  381. provider: &esv1beta1.AzureKVProvider{
  382. AuthType: &authType,
  383. VaultURL: &vaultURL,
  384. TenantID: pointer.To("mytenant"),
  385. AuthSecretRef: &esv1beta1.AzureKVAuth{
  386. ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo")},
  387. ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo")},
  388. },
  389. },
  390. },
  391. {
  392. name: "correct cluster secret store",
  393. objects: []client.Object{&corev1.Secret{
  394. ObjectMeta: metav1.ObjectMeta{
  395. Name: "password",
  396. Namespace: "foo",
  397. },
  398. Data: map[string][]byte{
  399. "id": []byte("foo"),
  400. "secret": []byte("bar"),
  401. },
  402. }},
  403. store: &esv1beta1.ClusterSecretStore{
  404. TypeMeta: metav1.TypeMeta{
  405. Kind: esv1beta1.ClusterSecretStoreKind,
  406. },
  407. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
  408. },
  409. provider: &esv1beta1.AzureKVProvider{
  410. AuthType: &authType,
  411. VaultURL: &vaultURL,
  412. TenantID: pointer.To("mytenant"),
  413. AuthSecretRef: &esv1beta1.AzureKVAuth{
  414. ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "secret"},
  415. ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.To("foo"), Key: "id"},
  416. },
  417. },
  418. },
  419. } {
  420. t.Run(row.name, func(t *testing.T) {
  421. k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
  422. spec := row.store.GetSpec()
  423. spec.Provider.AzureKV = row.provider
  424. az := &Azure{
  425. crClient: k8sClient,
  426. namespace: "default",
  427. provider: spec.Provider.AzureKV,
  428. store: row.store,
  429. }
  430. authorizer, err := az.authorizerForServicePrincipal(context.Background())
  431. if row.expErr == "" {
  432. tassert.Nil(t, err)
  433. tassert.NotNil(t, authorizer)
  434. } else {
  435. tassert.EqualError(t, err, row.expErr)
  436. }
  437. })
  438. }
  439. }
  440. func getTokenFromAuthorizer(t *testing.T, authorizer autorest.Authorizer) string {
  441. rq, _ := http.NewRequest("POST", "http://example.com", http.NoBody)
  442. _, err := authorizer.WithAuthorization()(
  443. autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
  444. return rq, nil
  445. })).Prepare(rq)
  446. tassert.Nil(t, err)
  447. return strings.TrimPrefix(rq.Header.Get("Authorization"), "Bearer ")
  448. }