Repository: avenga/couper
Pull request #133 (2021-03-10 15:00:25)

Fix upstream basic auth


Patch:
diff --git a/config/runtime/server.go b/config/runtime/server.go
index a9079d9b..a40e4293 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -311,7 +311,13 @@ func newBackend(evalCtx *hcl.EvalContext, backendCtx hcl.Body, log *logrus.Entry
 		return nil, err
 	}
 
-	backend := transport.NewBackend(backendCtx, tc, log, openAPIopts)
+	options := &transport.BackendOptions{
+		BasicAuth:  beConf.BasicAuth,
+		OpenAPI:    openAPIopts,
+		PathPrefix: beConf.PathPrefix,
+	}
+	backend := transport.NewBackend(backendCtx, tc, options, log)
+
 	return backend, nil
 }
 
diff --git a/handler/transport/backend.go b/handler/transport/backend.go
index 60832b05..5448040b 100644
--- a/handler/transport/backend.go
+++ b/handler/transport/backend.go
@@ -33,12 +33,12 @@ var _ http.RoundTripper = &Backend{}
 
 var ReClientSupportsGZ = regexp.MustCompile(`(?i)\b` + GzipName + `\b`)
 
-// Backend represents the transport <Backend> object.
+// Backend represents the transport configuration.
 type Backend struct {
-	accessControl    string // maps to basic-auth atm
 	context          hcl.Body
 	name             string
 	openAPIValidator *validation.OpenAPI
+	options          *BackendOptions
 	transportConf    *Config
 	upstreamLog      *logging.UpstreamLog
 	// oauth
@@ -47,18 +47,24 @@ type Backend struct {
 }
 
 // NewBackend creates a new <*Backend> object by the given <*Config>.
-func NewBackend(ctx hcl.Body, conf *Config, log *logrus.Entry, openAPIopts *validation.OpenAPIOptions) http.RoundTripper {
+func NewBackend(ctx hcl.Body, tc *Config, opts *BackendOptions, log *logrus.Entry) http.RoundTripper {
 	logEntry := log
-	if conf.BackendName != "" {
-		logEntry = log.WithField("backend", conf.BackendName)
+	if tc.BackendName != "" {
+		logEntry = log.WithField("backend", tc.BackendName)
+	}
+
+	var openAPI *validation.OpenAPI
+	if opts != nil {
+		openAPI = validation.NewOpenAPI(opts.OpenAPI)
 	}
 
 	backend := &Backend{
 		context:          ctx,
-		openAPIValidator: validation.NewOpenAPI(openAPIopts),
-		transportConf:    conf,
+		openAPIValidator: openAPI,
+		options:          opts,
+		transportConf:    tc,
 	}
-	backend.upstreamLog = logging.NewUpstreamLog(logEntry, backend, conf.NoProxyFromEnv)
+	backend.upstreamLog = logging.NewUpstreamLog(logEntry, backend, tc.NoProxyFromEnv)
 	return backend.upstreamLog
 }
 
@@ -87,8 +93,8 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
 
 	// oauth ....
 
-	if b.accessControl != "" {
-		auth := base64.StdEncoding.EncodeToString([]byte(b.accessControl))
+	if b.options != nil && b.options.BasicAuth != "" {
+		auth := base64.StdEncoding.EncodeToString([]byte(b.options.BasicAuth))
 		req.Header.Set("Authorization", "Basic "+auth)
 	}
 
diff --git a/handler/transport/backend_options.go b/handler/transport/backend_options.go
new file mode 100644
index 00000000..fef3cd02
--- /dev/null
+++ b/handler/transport/backend_options.go
@@ -0,0 +1,10 @@
+package transport
+
+import "github.com/avenga/couper/handler/validation"
+
+// BackendOptions represents the transport <BackendOptions> object.
+type BackendOptions struct {
+	BasicAuth  string
+	OpenAPI    *validation.OpenAPIOptions
+	PathPrefix string
+}


Test patch:
diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go
index f21f9db0..775a1ee3 100644
--- a/handler/endpoint_test.go
+++ b/handler/endpoint_test.go
@@ -80,7 +80,7 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
 
 			backend := transport.NewBackend(
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
-				&transport.Config{NoProxyFromEnv: true}, logger, nil)
+				&transport.Config{NoProxyFromEnv: true}, nil, logger)
 
 			ep := handler.NewEndpoint(&handler.EndpointOptions{
 				Error:        errors.DefaultJSON,
@@ -183,7 +183,7 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
 
 				backend := transport.NewBackend(
 					helper.NewProxyContext(tt.inlineCtx),
-					&transport.Config{NoProxyFromEnv: true}, logger, nil)
+					&transport.Config{NoProxyFromEnv: true}, nil, logger)
 
 				ep := handler.NewEndpoint(&handler.EndpointOptions{
 					Error:        errors.DefaultJSON,
@@ -268,7 +268,7 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
 
 			backend := transport.NewBackend(
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
-				&transport.Config{NoProxyFromEnv: true}, logger, nil)
+				&transport.Config{NoProxyFromEnv: true}, nil, logger)
 
 			ep := handler.NewEndpoint(&handler.EndpointOptions{
 				Error:        errors.DefaultJSON,
diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go
index 2eb17e7a..c41aface 100644
--- a/handler/transport/backend_test.go
+++ b/handler/transport/backend_test.go
@@ -59,7 +59,7 @@ func TestBackend_RoundTrip_Timings(t *testing.T) {
 			hook.Reset()
 
 			tt.tconf.NoProxyFromEnv = true // use origin addr from transport.Config
-			backend := transport.NewBackend(tt.context, tt.tconf, log, nil)
+			backend := transport.NewBackend(tt.context, tt.tconf, nil, log)
 
 			_, err := backend.RoundTrip(tt.req)
 			if err != nil && tt.expectedErr == "" {
@@ -95,7 +95,7 @@ func TestBackend_Compression_Disabled(t *testing.T) {
 			"origin": hcltest.MockExprLiteral(u),
 		}),
 	})
-	backend := transport.NewBackend(hclBody, &transport.Config{}, log, nil)
+	backend := transport.NewBackend(hclBody, &transport.Config{}, nil, log)
 
 	req := httptest.NewRequest(http.MethodOptions, "http://1.2.3.4/", nil)
 	res, err := backend.RoundTrip(req)
@@ -138,7 +138,7 @@ func TestBackend_Compression_ModifyAcceptEncoding(t *testing.T) {
 
 	backend := transport.NewBackend(hclBody, &transport.Config{
 		Origin: origin.URL,
-	}, log, nil)
+	}, nil, log)
 
 	req := httptest.NewRequest(http.MethodOptions, "http://1.2.3.4/", nil)
 	req.Header.Set("Accept-Encoding", "br, gzip")
@@ -236,7 +236,9 @@ func TestBackend_RoundTrip_Validation(t *testing.T) {
 				origin = "` + origin.URL + `"
 			`)
 
-			backend := transport.NewBackend(content, &transport.Config{}, log, openapiValidatorOptions)
+			backend := transport.NewBackend(content, &transport.Config{}, &transport.BackendOptions{
+				OpenAPI: openapiValidatorOptions,
+			}, log)
 
 			req := httptest.NewRequest(tt.requestMethod, "http://1.2.3.4"+tt.requestPath, nil)
 
@@ -313,7 +315,7 @@ func TestBackend_director(t *testing.T) {
 
 			backend := transport.NewBackend(hclContext, &transport.Config{
 				Timeout: time.Second,
-			}, nullLog, nil)
+			}, nil, nullLog)
 
 			req := httptest.NewRequest(http.MethodGet, "https://example.com"+tt.path, nil)
 			*req = *req.Clone(tt.ctx)
@@ -399,7 +401,9 @@ func TestProxy_BufferingOptions(t *testing.T) {
 			backend := transport.NewBackend(configload.MergeBodies([]hcl.Body{
 				test.NewRemainContext("origin", "http://"+origin.Listener.Addr().String()),
 				helper.NewProxyContext(tc.remain),
-			}), &transport.Config{}, nullLog, newOptions())
+			}), &transport.Config{}, &transport.BackendOptions{
+				OpenAPI: newOptions(),
+			}, nullLog)
 
 			upstreamLog := backend.(*logging.UpstreamLog)
 			backendHandler := reflect.ValueOf(upstreamLog).Elem().FieldByName("next")              // private field: ro
diff --git a/handler/validation/openapi_test.go b/handler/validation/openapi_test.go
index 0a8987b4..ead7d2e1 100644
--- a/handler/validation/openapi_test.go
+++ b/handler/validation/openapi_test.go
@@ -59,7 +59,9 @@ func TestOpenAPIValidator_ValidateRequest(t *testing.T) {
 	openAPI, err := validation.NewOpenAPIOptions(beConf.OpenAPI)
 	helper.Must(err)
 
-	backend := transport.NewBackend(beConf.Remain, &transport.Config{}, logger, openAPI)
+	backend := transport.NewBackend(beConf.Remain, &transport.Config{}, &transport.BackendOptions{
+		OpenAPI: openAPI,
+	}, logger)
 
 	tests := []struct {
 		name, path string
diff --git a/internal/test/helper_proxy.go b/internal/test/helper_proxy.go
index 7401aac6..0bb5f110 100644
--- a/internal/test/helper_proxy.go
+++ b/internal/test/helper_proxy.go
@@ -30,7 +30,7 @@ func (h *Helper) NewProxy(conf *transport.Config, backendContext, proxyContext h
 		proxyCtx = hcl.EmptyBody()
 	}
 
-	backend := transport.NewBackend(backendContext, config, logger.WithContext(context.Background()), nil)
+	backend := transport.NewBackend(backendContext, config, nil, logger.WithContext(context.Background()))
 
 	proxy := handler.NewProxy(backend, proxyCtx)
 	return proxy
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 915e72d8..6556fb91 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -1,6 +1,7 @@
 package server_test
 
 import (
+	"encoding/json"
 	"io/ioutil"
 	"net/http"
 	"path"
@@ -76,3 +77,38 @@ func TestEndpoints_Res(t *testing.T) {
 		t.Errorf("Expected body 'string', given %s", resBytes)
 	}
 }
+
+func TestEndpoints_UpstreamBasicAuth(t *testing.T) {
+	client := newClient()
+	helper := test.New(t)
+
+	shutdown, _ := newCouper(path.Join(testdataPath, "03_couper.hcl"), helper)
+	defer shutdown()
+
+	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/anything", nil)
+	helper.Must(err)
+
+	res, err := client.Do(req)
+	helper.Must(err)
+
+	if res.StatusCode != http.StatusOK {
+		t.Fatalf("Expected status 200, given %d", res.StatusCode)
+	}
+
+	resBytes, err := ioutil.ReadAll(res.Body)
+	helper.Must(err)
+	res.Body.Close()
+
+	type expectation struct {
+		Headers http.Header
+	}
+
+	var jsonResult expectation
+	err = json.Unmarshal(resBytes, &jsonResult)
+	helper.Must(err)
+
+	// The "dXNlcjpwYXNz" is base64encode("user:pass") from the HCL file.
+	if v := jsonResult.Headers.Get("Authorization"); v != "Basic dXNlcjpwYXNz" {
+		t.Errorf("Expected a valid 'Authorization' header, given '%s'", v)
+	}
+}
diff --git a/server/testdata/endpoints/03_couper.hcl b/server/testdata/endpoints/03_couper.hcl
new file mode 100644
index 00000000..7df0764e
--- /dev/null
+++ b/server/testdata/endpoints/03_couper.hcl
@@ -0,0 +1,13 @@
+server "backends" {
+  api {
+    endpoint "/anything" {
+      proxy {
+        backend {
+          origin = env.COUPER_TEST_BACKEND_ADDR
+
+          basic_auth = "user:pass"
+        }
+      }
+    }
+  }
+}