workload_identity_federation_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  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 secretmanager
  14. import (
  15. "context"
  16. "encoding/json"
  17. "testing"
  18. "github.com/stretchr/testify/assert"
  19. "golang.org/x/oauth2/google/externalaccount"
  20. authv1 "k8s.io/api/authentication/v1"
  21. corev1 "k8s.io/api/core/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/types"
  24. "sigs.k8s.io/controller-runtime/pkg/client"
  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. )
  29. type workloadIdentityFederationTest struct {
  30. name string
  31. wifConfig *esv1.GCPWorkloadIdentityFederation
  32. kubeObjects []client.Object
  33. genSAToken func(context.Context, []string, string, string) (*authv1.TokenRequest, error)
  34. expectError string
  35. expectTokenSource bool
  36. }
  37. const (
  38. testConfigMapName = "external-account-config"
  39. testConfigMapKey = "config.json"
  40. testServiceAccount = "test-sa"
  41. testAudience = "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/test-pool/providers/test-provider"
  42. testServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@test.iam.gserviceaccount.com:generateAccessToken"
  43. testSAToken = "test-sa-token"
  44. testAwsRegion = "us-west-2"
  45. // below values taken from https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html
  46. testAwsAccessKey = "AKIAIOSFODNN7EXAMPLE"
  47. testAwsSecretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  48. // below value taken from https://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html
  49. testAwsSessionToken = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE"
  50. testAwsTokenFQDNURL = "http://metadata.google.internal/latest/meta-data/iam/security-credentials"
  51. testAwsRegionFQDNURL = "http://metadata.google.internal/latest/meta-data/placement/availability-zone"
  52. testAwsSessionTokenFQDNURL = "http://metadata.google.internal/latest/api/token"
  53. testAwsTokenIPV4URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
  54. testAwsRegionIPv4URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
  55. testAwsSessionTokenIPv4URL = "http://169.254.169.254/latest/api/token"
  56. testAwsTokenIPV6URL = "http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials"
  57. testAwsRegionIPv6URL = "http://[fd00:ec2::254]/latest/meta-data/placement/availability-zone"
  58. testAwsSessionTokenIPv6URL = "http://[fd00:ec2::254]/latest/api/token"
  59. )
  60. var (
  61. testNamespace = "external-secrets-tests"
  62. )
  63. func createValidK8sExternalAccountConfig(audience string) string {
  64. config := map[string]interface{}{
  65. "type": externalAccountCredentialType,
  66. "audience": audience,
  67. "subject_token_type": workloadIdentitySubjectTokenType,
  68. "token_url": workloadIdentityTokenURL,
  69. "credential_source": map[string]interface{}{
  70. "file": "/var/run/secrets/oidc_token",
  71. },
  72. "token_info_url": workloadIdentityTokenInfoURL,
  73. }
  74. data, _ := json.Marshal(config)
  75. return string(data)
  76. }
  77. func createValidAWSExternalAccountConfig(audience string) string {
  78. config := map[string]interface{}{
  79. "type": externalAccountCredentialType,
  80. "audience": audience,
  81. "subject_token_type": workloadIdentitySubjectTokenType,
  82. "token_url": workloadIdentityTokenURL,
  83. "service_account_impersonation_url": testServiceAccountImpersonationURL,
  84. "credential_source": map[string]interface{}{
  85. "environment_id": "aws1",
  86. "url": testAwsTokenIPV4URL,
  87. "region_url": testAwsRegionIPv4URL,
  88. "imdsv2_session_token_url": testAwsSessionTokenIPv4URL,
  89. },
  90. }
  91. data, _ := json.Marshal(config)
  92. return string(data)
  93. }
  94. func createInvalidTypeExternalAccountConfig() string {
  95. config := map[string]interface{}{
  96. "type": "service_account",
  97. "audience": testAudience,
  98. }
  99. data, _ := json.Marshal(config)
  100. return string(data)
  101. }
  102. func createInvalidK8sExternalAccountConfigWithUnallowedTokenFilePath(audience string) string {
  103. config := map[string]interface{}{
  104. "type": externalAccountCredentialType,
  105. "audience": audience,
  106. "subject_token_type": workloadIdentitySubjectTokenType,
  107. "token_url": workloadIdentityTokenURL,
  108. "credential_source": map[string]interface{}{
  109. "file": autoMountedServiceAccountTokenPath,
  110. },
  111. "token_info_url": workloadIdentityTokenInfoURL,
  112. }
  113. data, _ := json.Marshal(config)
  114. return string(data)
  115. }
  116. func createInvalidK8sExternalAccountConfigWithUnallowedTokenURL(audience string) string {
  117. config := map[string]interface{}{
  118. "type": externalAccountCredentialType,
  119. "audience": audience,
  120. "subject_token_type": workloadIdentitySubjectTokenType,
  121. "token_url": "https://example.com",
  122. "credential_source": map[string]interface{}{
  123. "file": "/var/run/secrets/oidc_token",
  124. },
  125. "token_info_url": workloadIdentityTokenInfoURL,
  126. }
  127. data, _ := json.Marshal(config)
  128. return string(data)
  129. }
  130. func defaultSATokenGenerator(ctx context.Context, idPool []string, namespace, name string) (*authv1.TokenRequest, error) {
  131. return &authv1.TokenRequest{
  132. Status: authv1.TokenRequestStatus{
  133. Token: testSAToken,
  134. },
  135. }, nil
  136. }
  137. func TestWorkloadIdentityFederation(t *testing.T) {
  138. tests := []*workloadIdentityFederationTest{
  139. {
  140. name: "workload identity federation config is empty",
  141. wifConfig: nil,
  142. },
  143. {
  144. name: "invalid workload identity federation config without audience",
  145. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  146. ServiceAccountRef: &esmeta.ServiceAccountSelector{
  147. Name: testServiceAccount,
  148. Namespace: &testNamespace,
  149. Audiences: []string{testAudience},
  150. },
  151. },
  152. expectError: `invalid workloadIdentityFederation config: audience must be provided, when serviceAccountRef or awsSecurityCredentials is provided`,
  153. },
  154. {
  155. name: "successful kubernetes service account token federation",
  156. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  157. CredConfig: &esv1.ConfigMapReference{
  158. Name: testConfigMapName,
  159. Key: testConfigMapKey,
  160. Namespace: testNamespace,
  161. },
  162. },
  163. kubeObjects: []client.Object{
  164. &corev1.ConfigMap{
  165. ObjectMeta: metav1.ObjectMeta{
  166. Name: testConfigMapName,
  167. Namespace: testNamespace,
  168. },
  169. Data: map[string]string{
  170. testConfigMapKey: createValidK8sExternalAccountConfig(testAudience),
  171. },
  172. },
  173. },
  174. expectTokenSource: true,
  175. },
  176. {
  177. name: "cred configmap configured non-existent key",
  178. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  179. CredConfig: &esv1.ConfigMapReference{
  180. Name: testConfigMapName,
  181. Namespace: testNamespace,
  182. Key: testConfigMapKey,
  183. },
  184. },
  185. kubeObjects: []client.Object{
  186. &corev1.ConfigMap{
  187. ObjectMeta: metav1.ObjectMeta{
  188. Name: testConfigMapName,
  189. Namespace: testNamespace,
  190. },
  191. Data: map[string]string{
  192. "incorrect": createValidK8sExternalAccountConfig(testAudience),
  193. },
  194. },
  195. },
  196. expectError: `missing key "config.json" in configmap "external-account-config"`,
  197. },
  198. {
  199. name: "cred configmap configured with wrong key name",
  200. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  201. CredConfig: &esv1.ConfigMapReference{
  202. Name: testConfigMapName,
  203. Key: "wrongKey",
  204. Namespace: testNamespace,
  205. },
  206. },
  207. kubeObjects: []client.Object{
  208. &corev1.ConfigMap{
  209. ObjectMeta: metav1.ObjectMeta{
  210. Name: testConfigMapName,
  211. Namespace: testNamespace,
  212. },
  213. Data: map[string]string{
  214. testConfigMapKey: createValidK8sExternalAccountConfig(testAudience),
  215. },
  216. },
  217. },
  218. expectError: `missing key "wrongKey" in configmap "external-account-config"`,
  219. },
  220. {
  221. name: "invalid cred config - invalid tokenURL",
  222. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  223. CredConfig: &esv1.ConfigMapReference{
  224. Name: testConfigMapName,
  225. Namespace: testNamespace,
  226. Key: testConfigMapKey,
  227. },
  228. },
  229. kubeObjects: []client.Object{
  230. &corev1.ConfigMap{
  231. ObjectMeta: metav1.ObjectMeta{
  232. Name: testConfigMapName,
  233. Namespace: testNamespace,
  234. },
  235. Data: map[string]string{
  236. testConfigMapKey: createInvalidK8sExternalAccountConfigWithUnallowedTokenURL(testAudience),
  237. },
  238. },
  239. },
  240. expectError: "invalid external_account config\ntoken_url \"https://example.com\" must match https://sts.googleapis.com/v1/token",
  241. },
  242. {
  243. name: "successful AWS federation with security credentials",
  244. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  245. CredConfig: &esv1.ConfigMapReference{
  246. Name: testConfigMapName,
  247. Key: testConfigMapKey,
  248. Namespace: testNamespace,
  249. },
  250. },
  251. kubeObjects: []client.Object{
  252. &corev1.ConfigMap{
  253. ObjectMeta: metav1.ObjectMeta{
  254. Name: testConfigMapName,
  255. Namespace: testNamespace,
  256. },
  257. Data: map[string]string{
  258. testConfigMapKey: createValidAWSExternalAccountConfig(testAudience),
  259. },
  260. },
  261. &corev1.Secret{
  262. ObjectMeta: metav1.ObjectMeta{
  263. Name: "aws-creds",
  264. Namespace: testNamespace,
  265. },
  266. Data: map[string][]byte{
  267. awsAccessKeyIDKeyName: []byte(testAwsAccessKey),
  268. awsSecretAccessKeyKeyName: []byte(testAwsSecretKey),
  269. awsSessionTokenKeyName: []byte(testAwsSessionToken),
  270. },
  271. },
  272. },
  273. expectTokenSource: true,
  274. },
  275. {
  276. name: "external account creds configmap not present",
  277. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  278. CredConfig: &esv1.ConfigMapReference{
  279. Name: testConfigMapName,
  280. Key: testConfigMapKey,
  281. Namespace: testNamespace,
  282. },
  283. },
  284. kubeObjects: []client.Object{},
  285. expectError: `failed to fetch external acccount credentials configmap "external-secrets-tests/external-account-config": configmaps "external-account-config" not found`,
  286. },
  287. {
  288. name: "creds configmap has invalid type",
  289. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  290. CredConfig: &esv1.ConfigMapReference{
  291. Name: testConfigMapName,
  292. Key: testConfigMapKey,
  293. Namespace: testNamespace,
  294. },
  295. },
  296. kubeObjects: []client.Object{
  297. &corev1.ConfigMap{
  298. ObjectMeta: metav1.ObjectMeta{
  299. Name: testConfigMapName,
  300. Namespace: testNamespace,
  301. },
  302. Data: map[string]string{
  303. testConfigMapKey: createInvalidTypeExternalAccountConfig(),
  304. },
  305. },
  306. },
  307. expectError: `invalid credentials: 'type' field is "service_account" (expected "external_account")`,
  308. },
  309. {
  310. name: "creds configmap has non-json data",
  311. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  312. CredConfig: &esv1.ConfigMapReference{
  313. Name: testConfigMapName,
  314. Key: testConfigMapKey,
  315. Namespace: testNamespace,
  316. },
  317. },
  318. kubeObjects: []client.Object{
  319. &corev1.ConfigMap{
  320. ObjectMeta: metav1.ObjectMeta{
  321. Name: testConfigMapName,
  322. Namespace: testNamespace,
  323. },
  324. Data: map[string]string{
  325. testConfigMapKey: "invalid-json",
  326. },
  327. },
  328. },
  329. expectError: "failed to unmarshal external acccount config in \"external-account-config\": invalid character 'i' looking for beginning of value",
  330. },
  331. {
  332. name: "successful with service account reference",
  333. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  334. ServiceAccountRef: &esmeta.ServiceAccountSelector{
  335. Name: testServiceAccount,
  336. Namespace: &testNamespace,
  337. Audiences: []string{testAudience},
  338. },
  339. Audience: testAudience,
  340. },
  341. kubeObjects: []client.Object{
  342. &corev1.ConfigMap{
  343. ObjectMeta: metav1.ObjectMeta{
  344. Name: testConfigMapName,
  345. Namespace: testNamespace,
  346. },
  347. Data: map[string]string{
  348. testConfigMapKey: createInvalidK8sExternalAccountConfigWithUnallowedTokenFilePath(testAudience),
  349. },
  350. },
  351. },
  352. genSAToken: func(c context.Context, s1 []string, s2, s3 string) (*authv1.TokenRequest, error) {
  353. return &authv1.TokenRequest{
  354. Status: authv1.TokenRequestStatus{
  355. Token: testSAToken,
  356. },
  357. }, nil
  358. },
  359. expectTokenSource: true,
  360. },
  361. {
  362. name: "valid AWS credentials secret",
  363. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  364. AwsSecurityCredentials: &esv1.AwsCredentialsConfig{
  365. Region: testAwsRegion,
  366. AwsCredentialsSecretRef: &esv1.SecretReference{
  367. Name: "aws-creds",
  368. Namespace: testNamespace,
  369. },
  370. },
  371. Audience: testAudience,
  372. },
  373. kubeObjects: []client.Object{
  374. &corev1.ConfigMap{
  375. ObjectMeta: metav1.ObjectMeta{
  376. Name: testConfigMapName,
  377. Namespace: testNamespace,
  378. },
  379. Data: map[string]string{
  380. testConfigMapKey: createValidAWSExternalAccountConfig(testAudience),
  381. },
  382. },
  383. &corev1.Secret{
  384. ObjectMeta: metav1.ObjectMeta{
  385. Name: "aws-creds",
  386. Namespace: testNamespace,
  387. },
  388. Data: map[string][]byte{
  389. awsAccessKeyIDKeyName: []byte(testAwsAccessKey),
  390. awsSecretAccessKeyKeyName: []byte(testAwsSecretKey),
  391. },
  392. },
  393. },
  394. },
  395. {
  396. name: "non-existent AWS credentials secret",
  397. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  398. AwsSecurityCredentials: &esv1.AwsCredentialsConfig{
  399. Region: testAwsRegion,
  400. AwsCredentialsSecretRef: &esv1.SecretReference{
  401. Name: "non-existent-aws-creds",
  402. Namespace: testNamespace,
  403. },
  404. },
  405. Audience: testAudience,
  406. },
  407. kubeObjects: []client.Object{
  408. &corev1.ConfigMap{
  409. ObjectMeta: metav1.ObjectMeta{
  410. Name: testConfigMapName,
  411. Namespace: testNamespace,
  412. },
  413. Data: map[string]string{
  414. testConfigMapKey: createValidAWSExternalAccountConfig(testAudience),
  415. },
  416. },
  417. },
  418. expectError: `failed to fetch AwsSecurityCredentials secret "external-secrets-tests/non-existent-aws-creds": secrets "non-existent-aws-creds" not found`,
  419. },
  420. {
  421. name: "invalid AWS credentials - aws_access_key_id not provided",
  422. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  423. AwsSecurityCredentials: &esv1.AwsCredentialsConfig{
  424. Region: testAwsRegion,
  425. AwsCredentialsSecretRef: &esv1.SecretReference{
  426. Name: "aws-creds",
  427. Namespace: testNamespace,
  428. },
  429. },
  430. Audience: testAudience,
  431. },
  432. kubeObjects: []client.Object{
  433. &corev1.ConfigMap{
  434. ObjectMeta: metav1.ObjectMeta{
  435. Name: testConfigMapName,
  436. Namespace: testNamespace,
  437. },
  438. Data: map[string]string{
  439. testConfigMapKey: createValidAWSExternalAccountConfig(testAudience),
  440. },
  441. },
  442. &corev1.Secret{
  443. ObjectMeta: metav1.ObjectMeta{
  444. Name: "aws-creds",
  445. Namespace: testNamespace,
  446. },
  447. Data: map[string][]byte{
  448. awsSecretAccessKeyKeyName: []byte(testAwsSecretKey),
  449. },
  450. },
  451. },
  452. expectError: "aws_access_key_id and aws_secret_access_key keys must be present in AwsSecurityCredentials secret",
  453. },
  454. {
  455. name: "credConfig is empty",
  456. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  457. CredConfig: nil,
  458. },
  459. expectError: "invalid workloadIdentityFederation config: exactly one of credConfig, awsSecurityCredentials or serviceAccountRef must be provided",
  460. },
  461. {
  462. name: "both credential_source in credConfig and AwsCredentialsConfig are set",
  463. wifConfig: &esv1.GCPWorkloadIdentityFederation{
  464. CredConfig: &esv1.ConfigMapReference{
  465. Name: testConfigMapName,
  466. Key: testConfigMapKey,
  467. Namespace: testNamespace,
  468. },
  469. AwsSecurityCredentials: &esv1.AwsCredentialsConfig{
  470. Region: testAwsRegion,
  471. AwsCredentialsSecretRef: &esv1.SecretReference{
  472. Name: "aws-creds",
  473. Namespace: testNamespace,
  474. },
  475. },
  476. },
  477. kubeObjects: []client.Object{
  478. &corev1.ConfigMap{
  479. ObjectMeta: metav1.ObjectMeta{
  480. Name: testConfigMapName,
  481. Namespace: testNamespace,
  482. },
  483. Data: map[string]string{
  484. testConfigMapKey: createValidAWSExternalAccountConfig(testAudience),
  485. },
  486. },
  487. &corev1.Secret{
  488. ObjectMeta: metav1.ObjectMeta{
  489. Name: "aws-creds",
  490. Namespace: testNamespace,
  491. },
  492. Data: map[string][]byte{
  493. awsAccessKeyIDKeyName: []byte(testAwsAccessKey),
  494. awsSecretAccessKeyKeyName: []byte(testAwsSecretKey),
  495. awsSessionTokenKeyName: []byte(testAwsSessionToken),
  496. },
  497. },
  498. },
  499. expectError: "invalid workloadIdentityFederation config: exactly one of credConfig, awsSecurityCredentials or serviceAccountRef must be provided",
  500. },
  501. }
  502. for _, tc := range tests {
  503. t.Run(tc.name, func(t *testing.T) {
  504. fakeClient := clientfake.NewClientBuilder().WithObjects(tc.kubeObjects...).Build()
  505. fakeSATG := &fakeSATokenGen{
  506. GenerateFunc: tc.genSAToken,
  507. }
  508. if tc.genSAToken == nil {
  509. fakeSATG.GenerateFunc = defaultSATokenGenerator
  510. }
  511. wif := &workloadIdentityFederation{
  512. kubeClient: fakeClient,
  513. saTokenGenerator: fakeSATG,
  514. config: tc.wifConfig,
  515. isClusterKind: true,
  516. namespace: testNamespace,
  517. }
  518. ts, err := wif.TokenSource(context.Background())
  519. if tc.expectError != "" {
  520. assert.Error(t, err)
  521. assert.Equal(t, tc.expectError, err.Error())
  522. assert.Nil(t, ts)
  523. } else {
  524. assert.NoError(t, err)
  525. if tc.expectTokenSource {
  526. assert.NotNil(t, ts)
  527. }
  528. }
  529. })
  530. }
  531. }
  532. func TestValidateCredConfig(t *testing.T) {
  533. tests := []struct {
  534. name string
  535. config *externalaccount.Config
  536. wif *esv1.GCPWorkloadIdentityFederation
  537. expectError string
  538. }{
  539. {
  540. name: "valid kubernetes provider config",
  541. config: &externalaccount.Config{
  542. Audience: testAudience,
  543. SubjectTokenType: workloadIdentitySubjectTokenType,
  544. TokenURL: workloadIdentityTokenURL,
  545. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  546. CredentialSource: &externalaccount.CredentialSource{
  547. File: autoMountedServiceAccountTokenPath,
  548. },
  549. },
  550. wif: &esv1.GCPWorkloadIdentityFederation{
  551. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  552. },
  553. expectError: "",
  554. },
  555. {
  556. name: "valid AWS provider config with IPv6",
  557. config: &externalaccount.Config{
  558. Audience: testAudience,
  559. SubjectTokenType: workloadIdentitySubjectTokenType,
  560. TokenURL: workloadIdentityTokenURL,
  561. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  562. CredentialSource: &externalaccount.CredentialSource{
  563. EnvironmentID: "aws1",
  564. URL: testAwsTokenIPV6URL,
  565. RegionURL: testAwsRegionIPv6URL,
  566. IMDSv2SessionTokenURL: testAwsSessionTokenIPv6URL,
  567. },
  568. },
  569. wif: &esv1.GCPWorkloadIdentityFederation{
  570. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  571. },
  572. expectError: "",
  573. },
  574. {
  575. name: "valid AWS provider config with FQDN",
  576. config: &externalaccount.Config{
  577. Audience: testAudience,
  578. SubjectTokenType: workloadIdentitySubjectTokenType,
  579. TokenURL: workloadIdentityTokenURL,
  580. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  581. CredentialSource: &externalaccount.CredentialSource{
  582. EnvironmentID: "aws1",
  583. URL: testAwsTokenFQDNURL,
  584. RegionURL: testAwsRegionFQDNURL,
  585. IMDSv2SessionTokenURL: testAwsSessionTokenFQDNURL,
  586. },
  587. },
  588. wif: &esv1.GCPWorkloadIdentityFederation{
  589. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  590. },
  591. expectError: "",
  592. },
  593. {
  594. name: "invalid service account impersonation URL",
  595. config: &externalaccount.Config{
  596. Audience: testAudience,
  597. TokenURL: workloadIdentityTokenURL,
  598. ServiceAccountImpersonationURL: "https://invalid-url.com",
  599. },
  600. wif: &esv1.GCPWorkloadIdentityFederation{
  601. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  602. },
  603. expectError: "invalid external_account config\nservice_account_impersonation_url \"https://invalid-url.com\" does not have expected value",
  604. },
  605. {
  606. name: "invalid token URL",
  607. config: &externalaccount.Config{
  608. Audience: testAudience,
  609. TokenURL: "https://invalid-token-url.com",
  610. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  611. },
  612. wif: &esv1.GCPWorkloadIdentityFederation{
  613. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  614. },
  615. expectError: "invalid external_account config\ntoken_url \"https://invalid-token-url.com\" must match https://sts.googleapis.com/v1/token",
  616. },
  617. {
  618. name: "executable is configured",
  619. config: &externalaccount.Config{
  620. Audience: testAudience,
  621. TokenURL: workloadIdentityTokenURL,
  622. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  623. CredentialSource: &externalaccount.CredentialSource{
  624. Executable: &externalaccount.ExecutableConfig{
  625. Command: "/usr/local/bin/token-issuer",
  626. },
  627. },
  628. },
  629. wif: &esv1.GCPWorkloadIdentityFederation{
  630. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  631. },
  632. expectError: "invalid external_account config\ncredential_source.executable.command is not allowed\none of credential_source.file, credential_source.url, credential_source.aws.url or credential_source_environment_id should be provided",
  633. },
  634. {
  635. name: "invalid config - empty audience",
  636. config: &externalaccount.Config{
  637. TokenURL: workloadIdentityTokenURL,
  638. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  639. CredentialSource: &externalaccount.CredentialSource{
  640. File: "/var/run/secrets/token",
  641. },
  642. },
  643. wif: &esv1.GCPWorkloadIdentityFederation{
  644. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  645. },
  646. expectError: "invalid external_account config\naudience is empty",
  647. },
  648. {
  649. name: "invalid config - invalid URL",
  650. config: &externalaccount.Config{
  651. Audience: testAudience,
  652. TokenURL: workloadIdentityTokenURL,
  653. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  654. CredentialSource: &externalaccount.CredentialSource{
  655. URL: "https://example.com",
  656. },
  657. },
  658. wif: &esv1.GCPWorkloadIdentityFederation{
  659. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  660. ExternalTokenEndpoint: "https://mismatch.com",
  661. },
  662. expectError: "invalid external_account config\ncredential_source.url \"https://example.com\" does not match with the configured https://mismatch.com externalTokenEndpoint",
  663. },
  664. {
  665. name: "invalid config - invalid AWS config",
  666. config: &externalaccount.Config{
  667. Audience: testAudience,
  668. TokenURL: workloadIdentityTokenURL,
  669. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  670. CredentialSource: &externalaccount.CredentialSource{
  671. EnvironmentID: "sample",
  672. URL: "https://aws-token.com",
  673. RegionURL: "https://region.com",
  674. IMDSv2SessionTokenURL: "https://session-token.com",
  675. },
  676. },
  677. wif: &esv1.GCPWorkloadIdentityFederation{
  678. CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
  679. ExternalTokenEndpoint: "https://mismatch.com",
  680. },
  681. expectError: "invalid external_account config\ncredential_source.environment_id \"sample\" must start with aws\ncredential_source.aws.url \"https://aws-token.com\" does not have expected value\ncredential_source.aws.region_url \"https://region.com\" does not have expected value\ncredential_source.aws.imdsv2_session_token_url \"https://session-token.com\" does not have expected value",
  682. },
  683. }
  684. for _, tc := range tests {
  685. t.Run(tc.name, func(t *testing.T) {
  686. err := validateExternalAccountConfig(tc.config, tc.wif)
  687. if tc.expectError != "" {
  688. assert.Error(t, err)
  689. assert.Equal(t, tc.expectError, err.Error())
  690. } else {
  691. assert.NoError(t, err)
  692. }
  693. })
  694. }
  695. }
  696. func TestK8sSATokenReader(t *testing.T) {
  697. r := &k8sSATokenReader{
  698. audience: testAudience,
  699. subjectTokenType: workloadIdentitySubjectTokenType,
  700. saTokenGenerator: &fakeSATokenGen{
  701. GenerateFunc: defaultSATokenGenerator,
  702. },
  703. saAudience: []string{testAudience},
  704. serviceAccount: types.NamespacedName{
  705. Name: testServiceAccount,
  706. Namespace: testNamespace,
  707. },
  708. }
  709. ctx := context.Background()
  710. // Test successful token generation
  711. token, err := r.SubjectToken(ctx, externalaccount.SupplierOptions{
  712. Audience: testAudience,
  713. SubjectTokenType: workloadIdentitySubjectTokenType,
  714. })
  715. assert.NoError(t, err)
  716. assert.Equal(t, testSAToken, token)
  717. // Test invalid audience
  718. _, err = r.SubjectToken(ctx, externalaccount.SupplierOptions{
  719. Audience: "invalid-audience",
  720. SubjectTokenType: workloadIdentitySubjectTokenType,
  721. })
  722. assert.Error(t, err)
  723. assert.Equal(t,
  724. `invalid subject token request, audience is invalid-audience(expected //iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/test-pool/providers/test-provider) and subject_token_type is urn:ietf:params:oauth:token-type:jwt(expected urn:ietf:params:oauth:token-type:jwt)`,
  725. err.Error())
  726. // Test invalid subject token type
  727. _, err = r.SubjectToken(ctx, externalaccount.SupplierOptions{
  728. Audience: testAudience,
  729. SubjectTokenType: "invalid-type",
  730. })
  731. assert.Error(t, err)
  732. assert.Equal(t,
  733. `invalid subject token request, audience is //iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/test-pool/providers/test-provider(expected //iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/test-pool/providers/test-provider) and subject_token_type is invalid-type(expected urn:ietf:params:oauth:token-type:jwt)`,
  734. err.Error())
  735. }
  736. func TestAWSSecurityCredentialsReader(t *testing.T) {
  737. r := &awsSecurityCredentialsReader{
  738. region: testAwsRegion,
  739. awsSecurityCredentials: &externalaccount.AwsSecurityCredentials{
  740. AccessKeyID: testAwsAccessKey,
  741. SecretAccessKey: testAwsSecretKey,
  742. SessionToken: testAwsSessionToken,
  743. },
  744. }
  745. ctx := context.Background()
  746. options := externalaccount.SupplierOptions{}
  747. // Test region retrieval
  748. region, err := r.AwsRegion(ctx, options)
  749. assert.NoError(t, err)
  750. assert.Equal(t, testAwsRegion, region)
  751. // Test credentials retrieval
  752. creds, err := r.AwsSecurityCredentials(ctx, options)
  753. assert.NoError(t, err)
  754. assert.Equal(t, testAwsAccessKey, creds.AccessKeyID)
  755. assert.Equal(t, testAwsSecretKey, creds.SecretAccessKey)
  756. assert.Equal(t, testAwsSessionToken, creds.SessionToken)
  757. }
  758. func TestReadCredConfig(t *testing.T) {
  759. tests := []struct {
  760. name string
  761. config *esv1.GCPWorkloadIdentityFederation
  762. kubeObjects []client.Object
  763. expectError string
  764. }{
  765. {
  766. name: "cred configmap has empty data",
  767. config: &esv1.GCPWorkloadIdentityFederation{
  768. CredConfig: &esv1.ConfigMapReference{
  769. Name: testConfigMapName,
  770. Namespace: testNamespace,
  771. Key: testConfigMapKey,
  772. },
  773. },
  774. kubeObjects: []client.Object{
  775. &corev1.ConfigMap{
  776. ObjectMeta: metav1.ObjectMeta{
  777. Name: testConfigMapName,
  778. Namespace: testNamespace,
  779. },
  780. Data: map[string]string{
  781. testConfigMapKey: "",
  782. },
  783. },
  784. },
  785. expectError: `key "config.json" in configmap "external-account-config" has empty value`,
  786. },
  787. }
  788. for _, tc := range tests {
  789. t.Run(tc.name, func(t *testing.T) {
  790. fakeClient := clientfake.NewClientBuilder().WithObjects(tc.kubeObjects...).Build()
  791. wif := &workloadIdentityFederation{
  792. kubeClient: fakeClient,
  793. saTokenGenerator: &fakeSATokenGen{GenerateFunc: defaultSATokenGenerator},
  794. config: tc.config,
  795. isClusterKind: false,
  796. namespace: testNamespace,
  797. }
  798. ctx := context.Background()
  799. _, err := wif.readCredConfig(ctx)
  800. if tc.expectError != "" {
  801. assert.Error(t, err)
  802. assert.Equal(t, tc.expectError, err.Error())
  803. } else {
  804. assert.NoError(t, err)
  805. }
  806. })
  807. }
  808. }
  809. func TestGenerateExternalAccountConfig(t *testing.T) {
  810. wif := &esv1.GCPWorkloadIdentityFederation{
  811. CredConfig: &esv1.ConfigMapReference{
  812. Name: testConfigMapName,
  813. Namespace: testNamespace,
  814. },
  815. AwsSecurityCredentials: &esv1.AwsCredentialsConfig{
  816. Region: testAwsRegion,
  817. AwsCredentialsSecretRef: &esv1.SecretReference{
  818. Name: "aws-creds",
  819. Namespace: testNamespace,
  820. },
  821. },
  822. Audience: testAudience,
  823. }
  824. kubeObjects := []client.Object{
  825. &corev1.Secret{
  826. ObjectMeta: metav1.ObjectMeta{
  827. Name: "aws-creds",
  828. Namespace: testNamespace,
  829. },
  830. Data: map[string][]byte{
  831. awsAccessKeyIDKeyName: []byte(testAwsAccessKey),
  832. awsSecretAccessKeyKeyName: []byte(testAwsSecretKey),
  833. awsSessionTokenKeyName: []byte(testAwsSessionToken),
  834. },
  835. },
  836. }
  837. fakeClient := clientfake.NewClientBuilder().WithObjects(kubeObjects...).Build()
  838. wifInstance := &workloadIdentityFederation{
  839. kubeClient: fakeClient,
  840. saTokenGenerator: &fakeSATokenGen{GenerateFunc: defaultSATokenGenerator},
  841. config: wif,
  842. isClusterKind: false,
  843. namespace: testNamespace,
  844. }
  845. ctx := context.Background()
  846. credFile := &credentialsFile{
  847. Type: externalAccountCredentialType,
  848. Audience: testAudience,
  849. SubjectTokenType: workloadIdentitySubjectTokenType,
  850. TokenURLExternal: workloadIdentityTokenURL,
  851. ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
  852. }
  853. config, err := wifInstance.generateExternalAccountConfig(ctx, credFile)
  854. assert.NoError(t, err)
  855. assert.NotNil(t, config)
  856. assert.NotNil(t, config.AwsSecurityCredentialsSupplier)
  857. assert.Equal(t, testAudience, config.Audience)
  858. }