keyvault_auth_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. "k8s.io/utils/pointer"
  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. )
  71. // create a temporary file to imitate
  72. // azure workload identity webhook
  73. // see AZURE_FEDERATED_TOKEN_FILE
  74. tf, err := os.CreateTemp("", "")
  75. tassert.Nil(t, err)
  76. defer os.RemoveAll(tf.Name())
  77. _, err = tf.WriteString(saToken)
  78. tassert.Nil(t, err)
  79. tokenFile := tf.Name()
  80. authType := esv1beta1.AzureWorkloadIdentity
  81. defaultProvider := &esv1beta1.AzureKVProvider{
  82. VaultURL: &vaultURL,
  83. AuthType: &authType,
  84. ServiceAccountRef: &v1.ServiceAccountSelector{
  85. Name: saName,
  86. },
  87. }
  88. type testCase struct {
  89. name string
  90. provider *esv1beta1.AzureKVProvider
  91. k8sObjects []client.Object
  92. prep func(*testing.T)
  93. expErr string
  94. }
  95. for _, row := range []testCase{
  96. {
  97. name: "missing service account",
  98. provider: defaultProvider,
  99. expErr: "serviceaccounts \"" + saName + "\" not found",
  100. },
  101. {
  102. name: "missing webhook env vars",
  103. provider: &esv1beta1.AzureKVProvider{},
  104. expErr: "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set",
  105. },
  106. {
  107. name: "missing workload identity token file",
  108. provider: &esv1beta1.AzureKVProvider{},
  109. prep: func(t *testing.T) {
  110. t.Setenv("AZURE_CLIENT_ID", clientID)
  111. t.Setenv("AZURE_TENANT_ID", tenantID)
  112. t.Setenv("AZURE_FEDERATED_TOKEN_FILE", "invalid file")
  113. },
  114. expErr: "unable to read token file invalid file: open invalid file: no such file or directory",
  115. },
  116. {
  117. name: "correct workload identity",
  118. provider: &esv1beta1.AzureKVProvider{},
  119. prep: func(t *testing.T) {
  120. t.Setenv("AZURE_CLIENT_ID", clientID)
  121. t.Setenv("AZURE_TENANT_ID", tenantID)
  122. t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
  123. },
  124. },
  125. {
  126. name: "missing sa annotations",
  127. provider: defaultProvider,
  128. k8sObjects: []client.Object{
  129. &corev1.ServiceAccount{
  130. ObjectMeta: metav1.ObjectMeta{
  131. Name: saName,
  132. Namespace: namespace,
  133. Annotations: map[string]string{},
  134. },
  135. },
  136. },
  137. expErr: "missing service account annotation: azure.workload.identity/client-id",
  138. },
  139. {
  140. name: "successful case",
  141. provider: defaultProvider,
  142. k8sObjects: []client.Object{
  143. &corev1.ServiceAccount{
  144. ObjectMeta: metav1.ObjectMeta{
  145. Name: saName,
  146. Namespace: namespace,
  147. Annotations: map[string]string{
  148. AnnotationClientID: clientID,
  149. AnnotationTenantID: tenantID,
  150. },
  151. },
  152. },
  153. },
  154. },
  155. } {
  156. t.Run(row.name, func(t *testing.T) {
  157. store := esv1beta1.SecretStore{
  158. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
  159. AzureKV: row.provider,
  160. }},
  161. }
  162. k8sClient := clientfake.NewClientBuilder().
  163. WithObjects(row.k8sObjects...).
  164. Build()
  165. az := &Azure{
  166. store: &store,
  167. namespace: namespace,
  168. crClient: k8sClient,
  169. kubeClient: utilfake.NewCreateTokenMock().WithToken(saToken),
  170. provider: store.Spec.Provider.AzureKV,
  171. }
  172. tokenProvider := func(ctx context.Context, token, clientID, tenantID, aadEndpoint, kvResource string) (adal.OAuthTokenProvider, error) {
  173. tassert.Equal(t, token, saToken)
  174. tassert.Equal(t, clientID, clientID)
  175. tassert.Equal(t, tenantID, tenantID)
  176. return &tokenProvider{accessToken: azAccessToken}, nil
  177. }
  178. if row.prep != nil {
  179. row.prep(t)
  180. }
  181. authorizer, err := az.authorizerForWorkloadIdentity(context.Background(), tokenProvider)
  182. if row.expErr == "" {
  183. tassert.NotNil(t, authorizer)
  184. tassert.Equal(t, getTokenFromAuthorizer(t, authorizer), azAccessToken)
  185. } else {
  186. tassert.EqualError(t, err, row.expErr)
  187. }
  188. })
  189. }
  190. }
  191. func TestAuth(t *testing.T) {
  192. defaultStore := esv1beta1.SecretStore{
  193. ObjectMeta: metav1.ObjectMeta{
  194. Namespace: "default",
  195. },
  196. Spec: esv1beta1.SecretStoreSpec{
  197. Provider: &esv1beta1.SecretStoreProvider{},
  198. },
  199. }
  200. authType := esv1beta1.AzureServicePrincipal
  201. type testCase struct {
  202. name string
  203. provider *esv1beta1.AzureKVProvider
  204. store esv1beta1.GenericStore
  205. objects []client.Object
  206. expErr string
  207. }
  208. for _, row := range []testCase{
  209. {
  210. name: "bad config",
  211. expErr: "missing secretRef in provider config",
  212. store: &defaultStore,
  213. provider: &esv1beta1.AzureKVProvider{
  214. AuthType: &authType,
  215. VaultURL: &vaultURL,
  216. TenantID: pointer.StringPtr("mytenant"),
  217. },
  218. },
  219. {
  220. name: "bad config",
  221. expErr: "missing accessKeyID/secretAccessKey in store config",
  222. store: &defaultStore,
  223. provider: &esv1beta1.AzureKVProvider{
  224. AuthType: &authType,
  225. VaultURL: &vaultURL,
  226. TenantID: pointer.StringPtr("mytenant"),
  227. AuthSecretRef: &esv1beta1.AzureKVAuth{},
  228. },
  229. },
  230. {
  231. name: "bad config: missing secret",
  232. expErr: "could not find secret default/password: secrets \"password\" not found",
  233. store: &defaultStore,
  234. provider: &esv1beta1.AzureKVProvider{
  235. AuthType: &authType,
  236. VaultURL: &vaultURL,
  237. TenantID: pointer.StringPtr("mytenant"),
  238. AuthSecretRef: &esv1beta1.AzureKVAuth{
  239. ClientSecret: &v1.SecretKeySelector{Name: "password"},
  240. ClientID: &v1.SecretKeySelector{Name: "password"},
  241. },
  242. },
  243. },
  244. {
  245. name: "cluster secret store",
  246. expErr: "could not find secret foo/password: secrets \"password\" not found",
  247. store: &esv1beta1.ClusterSecretStore{
  248. TypeMeta: metav1.TypeMeta{
  249. Kind: esv1beta1.ClusterSecretStoreKind,
  250. },
  251. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
  252. },
  253. provider: &esv1beta1.AzureKVProvider{
  254. AuthType: &authType,
  255. VaultURL: &vaultURL,
  256. TenantID: pointer.StringPtr("mytenant"),
  257. AuthSecretRef: &esv1beta1.AzureKVAuth{
  258. ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo")},
  259. ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo")},
  260. },
  261. },
  262. },
  263. {
  264. name: "correct cluster secret store",
  265. objects: []client.Object{&corev1.Secret{
  266. ObjectMeta: metav1.ObjectMeta{
  267. Name: "password",
  268. Namespace: "foo",
  269. },
  270. Data: map[string][]byte{
  271. "id": []byte("foo"),
  272. "secret": []byte("bar"),
  273. },
  274. }},
  275. store: &esv1beta1.ClusterSecretStore{
  276. TypeMeta: metav1.TypeMeta{
  277. Kind: esv1beta1.ClusterSecretStoreKind,
  278. },
  279. Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
  280. },
  281. provider: &esv1beta1.AzureKVProvider{
  282. AuthType: &authType,
  283. VaultURL: &vaultURL,
  284. TenantID: pointer.StringPtr("mytenant"),
  285. AuthSecretRef: &esv1beta1.AzureKVAuth{
  286. ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo"), Key: "secret"},
  287. ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo"), Key: "id"},
  288. },
  289. },
  290. },
  291. } {
  292. t.Run(row.name, func(t *testing.T) {
  293. k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
  294. spec := row.store.GetSpec()
  295. spec.Provider.AzureKV = row.provider
  296. az := &Azure{
  297. crClient: k8sClient,
  298. namespace: "default",
  299. provider: spec.Provider.AzureKV,
  300. store: row.store,
  301. }
  302. authorizer, err := az.authorizerForServicePrincipal(context.Background())
  303. if row.expErr == "" {
  304. tassert.Nil(t, err)
  305. tassert.NotNil(t, authorizer)
  306. } else {
  307. tassert.EqualError(t, err, row.expErr)
  308. }
  309. })
  310. }
  311. }
  312. func getTokenFromAuthorizer(t *testing.T, authorizer autorest.Authorizer) string {
  313. rq, _ := http.NewRequest("POST", "http://example.com", http.NoBody)
  314. _, err := authorizer.WithAuthorization()(
  315. autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
  316. return rq, nil
  317. })).Prepare(rq)
  318. tassert.Nil(t, err)
  319. return strings.TrimPrefix(rq.Header.Get("Authorization"), "Bearer ")
  320. }