Repository: avenga/couper
Pull request #402 (2021-11-30 14:31:33)

Basic-Auth "user" accessable via request.context
Basic-Auth "user" is accessable via request.context

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b529430..82d68367 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
 
 * **Added**
   * [`custom_log_fields`](./docs/LOGS.md#custom-logging) attribute to be able to describe a user defined map for `custom` log field enrichment ([#388](https://github.com/avenga/couper/pull/388))
+  * * The `user` as context variable from a [Basic Auth](./docs/REFERENCE.md#basic-auth-block) is now accessable via `request.context.<label>.user` for successfully authenticated requests ([#402](https://github.com/avenga/couper/pull/402))
 
 * **Changed**
   * Missing [scope or roles claims](./docs/REFERENCE.md#jwt-block), or scope or roles claim with unsupported values are now ignored instead of causing an error ([#380](https://github.com/avenga/couper/issues/380))
diff --git a/accesscontrol/basic_auth.go b/accesscontrol/basic_auth.go
index c517ccf1..07e1d0c4 100644
--- a/accesscontrol/basic_auth.go
+++ b/accesscontrol/basic_auth.go
@@ -2,6 +2,7 @@ package accesscontrol
 
 import (
 	"bufio"
+	"context"
 	"crypto/subtle"
 	"fmt"
 	"net/http"
@@ -9,6 +10,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/errors"
 )
 
@@ -117,7 +119,7 @@ func (ba *BasicAuth) Validate(req *http.Request) error {
 	if ba.user == user {
 		if ba.pass != "" {
 			if subtle.ConstantTimeCompare([]byte(ba.pass), []byte(pass)) == 1 {
-				return nil
+				return ba.withUsername(req, user)
 			}
 			return errors.BasicAuth.Message("credential mismatch")
 		}
@@ -129,10 +131,27 @@ func (ba *BasicAuth) Validate(req *http.Request) error {
 
 	if len(ba.htFile) > 0 {
 		if validateAccessData(user, pass, ba.htFile) {
-			return nil
+			return ba.withUsername(req, user)
 		}
 		return errors.BasicAuth.Message("file: credential mismatch")
 	}
 
 	return errors.BasicAuth.Message("credential mismatch")
 }
+
+func (ba *BasicAuth) withUsername(req *http.Request, user string) error {
+	u := make(map[string]interface{})
+	u["user"] = user
+
+	ctx := req.Context()
+	acMap, ok := ctx.Value(request.AccessControls).(map[string]interface{})
+	if !ok {
+		acMap = make(map[string]interface{})
+	}
+	acMap[ba.name] = u
+
+	ctx = context.WithValue(ctx, request.AccessControls, acMap)
+	*req = *req.WithContext(ctx)
+
+	return nil
+}
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 032f24a4..9102c6d5 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -339,6 +339,8 @@ by `htpasswd_file` otherwise.
 | `realm`         | string | `""`    | The realm to be sent in a `WWW-Authenticate` response HTTP header field. | - | - |
 | `custom_log_fields` | map | - | Defines log fields for [Custom Logging](LOGS.md#custom-logging). | &#9888; Inherited by nested blocks. | - |
 
+The `user` is accessable via `request.context.<label>.user` for successfully authenticated requests.
+
 ### JWT Block
 
 The `jwt` block lets you configure JSON Web Token access control for your gateway.
@@ -608,9 +610,11 @@ defaults {
 
 The value of `context.<name>` depends on the type of block referenced by `<name>`.
 
-For a [JWT Block](#jwt-block), the variable contains claims from the JWT used for [Access Control](#access-control).
+For a [Basic Auth](#basic-auth-block) and successfully authenticated request the variable contains the `user` name.
+
+For a [JWT Block](#jwt-block) the variable contains claims from the JWT used for [Access Control](#access-control).
 
-For a [SAML Block](#saml-block), the variable contains
+For a [SAML Block](#saml-block) the variable contains
 
 - `sub`: the `NameID` of the SAML assertion
 - `exp`: optional expiration date (value of `SessionNotOnOrAfter` of the SAML assertion)


Test patch:
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 232ee036..d550f0a9 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3026,6 +3026,7 @@ func TestConfigBodyContentAccessControl(t *testing.T) {
 		{"/v5/not-exist", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"},
 		{"/superadmin", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK, "application/json", ""},
 		{"/superadmin", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"},
+		{"/ba5", http.Header{"Authorization": []string{"Basic VVNSOlBXRA=="}, "X-Ba-User": []string{"USR"}}, http.StatusOK, "application/json", ""},
 		{"/v4", http.Header{}, http.StatusUnauthorized, "text/html", "access control error: ba1: credentials required"},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
@@ -3041,6 +3042,7 @@ func TestConfigBodyContentAccessControl(t *testing.T) {
 
 			res, err := client.Do(req)
 			helper.Must(err)
+			// t.Errorf(">>> %#v", res.Header)
 
 			message := getAccessControlMessages(hook)
 			if tc.wantErrLog == "" {
diff --git a/server/testdata/integration/config/03_couper.hcl b/server/testdata/integration/config/03_couper.hcl
index 5f970891..633e43e0 100644
--- a/server/testdata/integration/config/03_couper.hcl
+++ b/server/testdata/integration/config/03_couper.hcl
@@ -84,6 +84,22 @@ server "acs" {
     }
   }
 
+  endpoint "/ba5" {
+    access_control = ["ba5"]
+    disable_access_control = ["ba1"]
+    proxy {
+      backend "test" {
+        set_request_headers = {
+          X-Ba-User = request.context.ba5.user
+          Authorization = request.headers.authorization # proxy blacklist
+        }
+        set_response_headers = {
+          X-BA-User = request.context.ba5.user
+        }
+      }
+    }
+  }
+
   endpoint "/jwt" {
     disable_access_control = ["ba1"]
     access_control = ["JWTToken"]
@@ -200,6 +216,10 @@ definitions {
   basic_auth "ba4" {
     password = "asdf"
   }
+  basic_auth "ba5" {
+    user     = "USR"
+    password = "PWD"
+  }
   jwt "JWTToken" {
     header = "Authorization"
     signature_algorithm = "HS256"