token_manager_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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 oidc
  14. import (
  15. "context"
  16. "fmt"
  17. "testing"
  18. "time"
  19. "github.com/stretchr/testify/assert"
  20. "github.com/stretchr/testify/require"
  21. authv1 "k8s.io/api/authentication/v1"
  22. "k8s.io/apimachinery/pkg/runtime"
  23. "k8s.io/client-go/kubernetes/fake"
  24. k8stesting "k8s.io/client-go/testing"
  25. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  26. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  27. )
  28. // noopExchanger satisfies TokenExchanger and returns a fixed token.
  29. type noopExchanger struct{}
  30. func (n *noopExchanger) ExchangeToken(_ context.Context, _ string) (string, time.Time, error) {
  31. return "exchanged-token", time.Now().Add(time.Hour), nil
  32. }
  33. // namespaceMismatchReactor intercepts serviceaccounts/token create calls and
  34. // returns an error when the request body namespace differs from the URL namespace,
  35. // mimicking the kube-apiserver's enforcement. fake.NewSimpleClientset() does not
  36. // enforce this check, which is why the pre-fix code passed tests.
  37. func namespaceMismatchReactor(action k8stesting.Action) (bool, runtime.Object, error) {
  38. ca, ok := action.(k8stesting.CreateAction)
  39. if !ok {
  40. return false, nil, nil
  41. }
  42. tr, ok := ca.GetObject().(*authv1.TokenRequest)
  43. if !ok {
  44. return false, nil, nil
  45. }
  46. urlNS := action.GetNamespace()
  47. bodyNS := tr.Namespace
  48. if bodyNS != "" && bodyNS != urlNS {
  49. return true, nil, fmt.Errorf(
  50. "the namespace of the provided object does not match "+
  51. "the namespace sent on the request: body=%q url=%q",
  52. bodyNS, urlNS,
  53. )
  54. }
  55. return true, &authv1.TokenRequest{
  56. Status: authv1.TokenRequestStatus{Token: "fake-sa-token"},
  57. }, nil
  58. }
  59. func TestCreateServiceAccountToken_NamespaceConsistency(t *testing.T) {
  60. tests := []struct {
  61. name string
  62. storeKind string
  63. esNamespace string
  64. saRef esmeta.ServiceAccountSelector
  65. wantNamespace string // expected URL (and body) namespace
  66. }{
  67. {
  68. name: "SecretStore same-namespace",
  69. storeKind: esv1.SecretStoreKind,
  70. esNamespace: "default",
  71. saRef: esmeta.ServiceAccountSelector{Name: "my-sa"},
  72. wantNamespace: "default",
  73. },
  74. {
  75. name: "ClusterSecretStore cross-namespace regression",
  76. storeKind: esv1.ClusterSecretStoreKind,
  77. esNamespace: "default",
  78. saRef: esmeta.ServiceAccountSelector{Name: "oidc-sa", Namespace: new("external-secrets")},
  79. // Pre-fix: body namespace was "default", URL was "external-secrets"
  80. // -> kube-apiserver rejected with "namespace of provided object does not match".
  81. wantNamespace: "external-secrets",
  82. },
  83. {
  84. name: "ClusterSecretStore SA namespace equals ES namespace",
  85. storeKind: esv1.ClusterSecretStoreKind,
  86. esNamespace: "external-secrets",
  87. saRef: esmeta.ServiceAccountSelector{Name: "oidc-sa", Namespace: new("external-secrets")},
  88. wantNamespace: "external-secrets",
  89. },
  90. {
  91. name: "ClusterSecretStore no explicit SA namespace falls back to ES namespace",
  92. storeKind: esv1.ClusterSecretStoreKind,
  93. esNamespace: "default",
  94. saRef: esmeta.ServiceAccountSelector{Name: "oidc-sa"},
  95. wantNamespace: "default",
  96. },
  97. }
  98. for _, tc := range tests {
  99. t.Run(tc.name, func(t *testing.T) {
  100. capturedURLNS := ""
  101. capturedBodyNS := ""
  102. fc := fake.NewSimpleClientset()
  103. fc.Fake.PrependReactor("create", "serviceaccounts/token",
  104. func(action k8stesting.Action) (bool, runtime.Object, error) {
  105. capturedURLNS = action.GetNamespace()
  106. ca, ok := action.(k8stesting.CreateAction)
  107. if ok {
  108. if tr, ok := ca.GetObject().(*authv1.TokenRequest); ok {
  109. capturedBodyNS = tr.Namespace
  110. }
  111. }
  112. return namespaceMismatchReactor(action)
  113. },
  114. )
  115. m := NewBaseTokenManager(
  116. fc.CoreV1(), tc.esNamespace, tc.storeKind, "https://example.com", tc.saRef,
  117. )
  118. saToken, err := m.CreateServiceAccountToken(context.Background())
  119. require.NoError(t, err)
  120. assert.Equal(t, "fake-sa-token", saToken)
  121. assert.Equal(t, tc.wantNamespace, capturedURLNS,
  122. "URL namespace must match the SA's namespace")
  123. assert.Equal(t, capturedURLNS, capturedBodyNS,
  124. "body namespace must equal URL namespace (apiserver enforces this)")
  125. })
  126. }
  127. }
  128. func TestGetToken_CachingAndRefresh(t *testing.T) {
  129. fc := fake.NewSimpleClientset()
  130. callCount := 0
  131. fc.Fake.PrependReactor("create", "serviceaccounts/token",
  132. func(_ k8stesting.Action) (bool, runtime.Object, error) {
  133. callCount++
  134. return true, &authv1.TokenRequest{
  135. Status: authv1.TokenRequestStatus{Token: fmt.Sprintf("token-%d", callCount)},
  136. }, nil
  137. },
  138. )
  139. saRef := esmeta.ServiceAccountSelector{Name: "test-sa"}
  140. m := NewBaseTokenManager(fc.CoreV1(), "default", esv1.SecretStoreKind, "https://example.com", saRef)
  141. m.Exchanger = &noopExchanger{}
  142. tok1, err := m.GetToken(context.Background())
  143. require.NoError(t, err)
  144. assert.Equal(t, "exchanged-token", tok1)
  145. assert.Equal(t, 1, callCount)
  146. // Second call should use the cached token without a new SA token request.
  147. tok2, err := m.GetToken(context.Background())
  148. require.NoError(t, err)
  149. assert.Equal(t, tok1, tok2)
  150. assert.Equal(t, 1, callCount, "cached token returned without a new SA token request")
  151. }
  152. func TestBuildAudiences(t *testing.T) {
  153. tests := []struct {
  154. name string
  155. saAudiences []string
  156. extraAud []string
  157. baseURL string
  158. wantAudience []string
  159. }{
  160. {
  161. name: "uses baseURL when no SA audiences configured",
  162. baseURL: "https://api.example.com",
  163. wantAudience: []string{"https://api.example.com"},
  164. },
  165. {
  166. name: "SA audiences override baseURL",
  167. saAudiences: []string{"sts.amazonaws.com"},
  168. baseURL: "https://api.example.com",
  169. wantAudience: []string{"sts.amazonaws.com"},
  170. },
  171. {
  172. name: "extra audiences appended after SA audiences",
  173. saAudiences: []string{"sts.amazonaws.com"},
  174. extraAud: []string{"extra-aud"},
  175. baseURL: "https://api.example.com",
  176. wantAudience: []string{"sts.amazonaws.com", "extra-aud"},
  177. },
  178. {
  179. name: "extra audiences appended after baseURL fallback",
  180. extraAud: []string{"extra-aud"},
  181. baseURL: "https://api.example.com",
  182. wantAudience: []string{"https://api.example.com", "extra-aud"},
  183. },
  184. }
  185. for _, tc := range tests {
  186. t.Run(tc.name, func(t *testing.T) {
  187. saRef := esmeta.ServiceAccountSelector{
  188. Name: "test-sa",
  189. Audiences: tc.saAudiences,
  190. }
  191. fc := fake.NewSimpleClientset()
  192. m := NewBaseTokenManager(fc.CoreV1(), "default", esv1.SecretStoreKind, tc.baseURL, saRef)
  193. m.ExtraAudiences = tc.extraAud
  194. assert.Equal(t, tc.wantAudience, m.BuildAudiences())
  195. })
  196. }
  197. }