webhook_auth_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. // /*
  14. // Licensed under the Apache License, Version 2.0 (the "License");
  15. // you may not use this file except in compliance with the License.
  16. // You may obtain a copy of the License at
  17. //
  18. // https://www.apache.org/licenses/LICENSE-2.0
  19. //
  20. // Unless required by applicable law or agreed to in writing, software
  21. // distributed under the License is distributed on an "AS IS" BASIS,
  22. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  23. // See the License for the specific language governing permissions and
  24. // limitations under the License.
  25. // */
  26. package webhook
  27. import (
  28. "context"
  29. b64 "encoding/base64"
  30. "net/http"
  31. "net/http/httptest"
  32. "testing"
  33. corev1 "k8s.io/api/core/v1"
  34. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  35. "sigs.k8s.io/controller-runtime/pkg/client"
  36. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  37. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  38. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  39. )
  40. type mockAuthTestPackage struct {
  41. Creds mockCreds
  42. MockServer mockAuthTestServer
  43. Request mockAuthRequest
  44. Expect string
  45. }
  46. type mockCreds struct {
  47. UserName string
  48. Password string
  49. }
  50. type mockAuthTestServer func(
  51. serverCreds mockCreds,
  52. t *testing.T) *httptest.Server
  53. type mockAuthRequest func(
  54. url string,
  55. creds mockCreds,
  56. t *testing.T) string
  57. func TestWebhookAuth(t *testing.T) {
  58. // define test cases
  59. creds := mockCreds{"correctuser123", "correctpassword123"}
  60. basicAuthExpect := "Basic " + b64.StdEncoding.EncodeToString([]byte(creds.UserName+":"+creds.Password))
  61. ntlmExpect := "NTLM TlRMTVNTUAABAAAAAQCIoAAAAAAoAAAAAAAAACgAAAAGAbEdAAAADw=="
  62. negotiateExpect := "Negotiate TlRMTVNTUAABAAAAAQCIoAAAAAAoAAAAAAAAACgAAAAGAbEdAAAADw=="
  63. // due to integrated nature of GetSecret(), we use a mock server
  64. // to return relevant parts of a request, in this case, the auth header.
  65. testAuthHeaders := map[string]mockAuthTestPackage{
  66. "BasicAuth": {creds, basicAuthRequestEcho, basicAuthRequest, basicAuthExpect},
  67. "NTLM": {creds, ntlmRequestEcho, ntlmRequest, ntlmExpect},
  68. "Negotiate": {creds, negotiateRequestEcho, ntlmRequest, negotiateExpect},
  69. }
  70. // execute test cases
  71. for _, p := range testAuthHeaders {
  72. server := p.MockServer(p.Creds, t)
  73. result := p.Request(server.URL, creds, t)
  74. server.Close()
  75. expect := p.Expect
  76. if result != expect {
  77. t.Errorf("Test failed. Result: '%s' / Expected: '%s'", result, expect)
  78. }
  79. }
  80. }
  81. func ntlmRequestEcho(mockCreds, *testing.T) *httptest.Server {
  82. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  83. reqAuthString := r.Header.Get("Authorization")
  84. if reqAuthString == "" {
  85. // go-ntlmssp first sends anonymous request, respond with 401
  86. w.Header().Add("WWW-Authenticate", "NTLM")
  87. w.WriteHeader(401)
  88. } else {
  89. w.Write([]byte(reqAuthString))
  90. }
  91. }))
  92. return server
  93. }
  94. func negotiateRequestEcho(mockCreds, *testing.T) *httptest.Server {
  95. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  96. reqAuthString := r.Header.Get("Authorization")
  97. if reqAuthString == "" {
  98. // go-ntlmssp first sends anonymous request, respond with 401
  99. w.Header().Add("WWW-Authenticate", "Negotiate")
  100. w.WriteHeader(401)
  101. } else {
  102. w.Write([]byte(reqAuthString))
  103. }
  104. }))
  105. return server
  106. }
  107. func ntlmRequest(url string, creds mockCreds, t *testing.T) string {
  108. secretName := "ntlmTestAuthSecret"
  109. secretNamespace := "default"
  110. // ntlm clustersecretstore takes credentials from a secret,
  111. // so we need to mock k8s-client retrieval of secret.
  112. mockClient := fake.NewClientBuilder().WithObjects(&corev1.Secret{
  113. ObjectMeta: metav1.ObjectMeta{
  114. Namespace: secretNamespace,
  115. Name: secretName,
  116. Labels: map[string]string{
  117. "external-secrets.io/type": "webhook",
  118. },
  119. },
  120. Data: map[string][]byte{
  121. "userName": []byte(creds.UserName),
  122. "password": []byte(creds.Password),
  123. },
  124. }).Build()
  125. // create clusteSecretStore
  126. ntlmAuthStore := &esv1.ClusterSecretStore{
  127. TypeMeta: metav1.TypeMeta{
  128. Kind: "ClusterSecretStore",
  129. },
  130. ObjectMeta: metav1.ObjectMeta{
  131. Name: "webhook-store",
  132. Namespace: secretNamespace,
  133. },
  134. Spec: esv1.SecretStoreSpec{
  135. Provider: &esv1.SecretStoreProvider{
  136. Webhook: &esv1.WebhookProvider{
  137. URL: url,
  138. Auth: &esv1.AuthorizationProtocol{
  139. NTLM: &esv1.NTLMProtocol{
  140. UserName: esmeta.SecretKeySelector{
  141. Name: secretName,
  142. Namespace: &secretNamespace,
  143. Key: "userName",
  144. },
  145. Password: esmeta.SecretKeySelector{
  146. Name: secretName,
  147. Namespace: &secretNamespace,
  148. Key: "password",
  149. },
  150. },
  151. },
  152. },
  153. },
  154. },
  155. }
  156. result := exerciseGetSecret(ntlmAuthStore, mockClient, t)
  157. return result
  158. }
  159. func basicAuthRequestEcho(mockCreds, *testing.T) *httptest.Server {
  160. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  161. reqAuthString := r.Header.Get("Authorization")
  162. if reqAuthString == "" {
  163. w.Write([]byte("Empty Authorization header"))
  164. } else {
  165. w.Write([]byte(reqAuthString))
  166. }
  167. }))
  168. return server
  169. }
  170. func basicAuthRequest(url string, creds mockCreds, t *testing.T) string {
  171. reqAuthString := "Basic " + b64.StdEncoding.EncodeToString([]byte(creds.UserName+":"+creds.Password))
  172. // create ClusterSecretStore
  173. basicAuthStore := &esv1.ClusterSecretStore{
  174. TypeMeta: metav1.TypeMeta{
  175. Kind: "ClusterSecretStore",
  176. },
  177. ObjectMeta: metav1.ObjectMeta{
  178. Name: "webhook-store",
  179. Namespace: "default",
  180. },
  181. Spec: esv1.SecretStoreSpec{
  182. Provider: &esv1.SecretStoreProvider{
  183. Webhook: &esv1.WebhookProvider{
  184. URL: url,
  185. Headers: map[string]string{
  186. "Authorization": reqAuthString,
  187. },
  188. },
  189. },
  190. },
  191. }
  192. result := exerciseGetSecret(basicAuthStore, nil, t)
  193. return result
  194. }
  195. func exerciseGetSecret(mockStore esv1.GenericStore, mockKubeClient client.Client, t *testing.T) string {
  196. mockProvider := &Provider{}
  197. client, err := mockProvider.NewClient(context.Background(), mockStore, mockKubeClient, "default")
  198. if err != nil {
  199. t.Errorf("Error creating client: %q", err)
  200. return "error"
  201. }
  202. // perform request, exercising GetSecret
  203. resp, err := client.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "dummy"})
  204. if err != nil {
  205. t.Errorf("Error retrieving secret:%s", err)
  206. }
  207. return string(resp)
  208. }