Repository: dadrus/heimdall (go)

Issue:
Add endpoint.values equivalent for payload of remote authorizer
### Preflight checklist

- [X] I agree to follow this project's [Code of Conduct](https://github.com/dadrus/heimdall/blob/main/CODE_OF_CONDUCT.md).
- [X] I have read and am following this repository's [Contribution Guidelines](https://github.com/dadrus/heimdall/blob/main/CONTRIBUTING.md)."
- [X] I have discussed this feature request with the community.

### Describe the background of your feature request

Currently, when wanting to change the payload of a `remote` authorizer on a per-pipeline basis, one needs to override the full payload string.
For an authorization API like [OpenFGA's](https://openfga.dev/api/service#/Relationship%20Queries/Check), this means that pipelines have to be defined as follows:
```yaml
- authorizer: openfga
  config:
    payload: |
      {
        "user": "user:{{ .Subject.ID }}",
        "relation": "read",
        "resource": "endpoint:foo"
      }
```
which is error-prone and repetitive.

### Describe your idea

The `remote` authorizer already has a feature that allows substituting values into the URL, like so:
```yaml
- id: foo
  config:
    endpoint:
      url: https://opa.local/v1/data/{{ .Values.namespace }}/{{ .Values.policy }}
      values:
        namespace: myapi/policy
        policy: allow_write
```
where the pipeline can then contain
```yaml
- authorizer: foo
  config:
    endpoint:
      values:
        policy: something_else
```
making it easier to replace parts of the URL without having to fully retype it each time.
I propose a similar mechanism for substituting values into the payload, where the above mentioned `openfga` authorizer could for example be defined as such:
```yaml
- id: openfga
  config:
    endpoint: # etc.
    payload: |
      {
        "user": "user:{{ .Subject.ID }}",
        "relation": {{ quote .Values.relation }},
        "resource": {{ quote .Values.resource }}
      }
    values: # default values
```
and then used in a pipeline like
```yaml
- authorizer: openfga
  config:
    values:
      relation: read
      resource: "endpoint:foo"
```
making it more difficult to accidentally mess up the payload.

### Are there any workarounds or alternatives?

Currently the workaround is what I described in the problem, i.e. always specifying the full payload.

A different way of implementing the solution I mentioned would be to use the same `values` object for both substituting into the URL and into the payload, as I don't really see a reason why they should be separated. In that case I'd remove the `values` key from the `endpoint` object and just have one directly in `config`.

### Version

n/a

### Additional Context

This feature was discussed in this Discord thread: https://discord.com/channels/1100447190796742698/1120438467411857558

Pull request description:
feat: Values object can be used in payload of generic contextualizer and remote authorizer
## Related issue(s)

closes #686

## Checklist

- [x] I agree to follow this project's [Code of Conduct](../CODE_OF_CONDUCT.md).
- [x] I have read, and I am following this repository's [Contributing Guidelines](../CONTRIBUTING.md).
- [x] I have read the [Security Policy](../SECURITY.md).
- [x] I have referenced an issue describing the bug/feature request.
- [x] I have added tests that prove the correctness of my implementation.
- [x] I have updated the documentation.

## Description

Makes following possible:

```yaml
- id: openfga
  config:
    endpoint: # etc.
    payload: |
      {
        "user": "user:{{ .Subject.ID }}",
        "relation": {{ quote .Values.relation }},
        "resource": {{ quote .Values.resource }}
      }
```

and then used in a pipeline like

```yaml
- authorizer: openfga
  config:
    values:
      relation: read
      resource: "endpoint:foo"
```

which will result in the following json payload sent to the defined endpoint for the `.Subject.ID` being `1`:

```json
{
    "user": "user:1",
    "relation": "read",
    "resource": "endpoint:foo"
}
```

If the `values` property is defined in the configuration of the particular mechanism, the corresponding entries act as defaults for the rule pipeline making use of that mechanism.


Patch:
diff --git a/docs/content/docs/configuration/rules/pipeline_mechanisms/authorizers.adoc b/docs/content/docs/configuration/rules/pipeline_mechanisms/authorizers.adoc
index 3cc119fe..04bdd0bd 100644
--- a/docs/content/docs/configuration/rules/pipeline_mechanisms/authorizers.adoc
+++ b/docs/content/docs/configuration/rules/pipeline_mechanisms/authorizers.adoc
@@ -134,7 +134,7 @@ Allows caching of the authorization endpoint responses. Defaults to 0s, which me
 
 * *`values`* _map of strings_ (optional, overridable)
 +
-A key value map, which is made accessible to the template rendering engine as link:{{< relref "overview.adoc#_values" >}}[`Values`] object, e.g. to render parts of the URL.
+A key value map, which is made accessible to the template rendering engine as link:{{< relref "overview.adoc#_values" >}}[`Values`] object, to render parts of the URL or the payload.
 
 .Configuration of Remote authorizer to communicate with https://www.openpolicyagent.org/[Open Policy Agent] (OPA)
 ====
@@ -156,7 +156,7 @@ config:
         user: ${OPA_USER}
         password: ${OPA_PASSWORD}
   payload: |
-    { "input": { "user": {{ quote .Subject.ID }} } }
+    { "input": { "user": {{ quote .Subject.ID }} }, "some_data": {{ quote .Values.whatever }} }
   values:
     namespace: myapi/policy
     policy: allow_write
@@ -187,6 +187,7 @@ A specific rule could then use this authorizer in the following ways:
     config: # overriding with rule specifics
       values:
         policy: allow_read
+        whatever: foo
   - # other mechanisms
 ----
 
diff --git a/docs/content/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc b/docs/content/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc
index d019ff81..7de159e9 100644
--- a/docs/content/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc
+++ b/docs/content/docs/configuration/rules/pipeline_mechanisms/contextualizers.adoc
@@ -49,9 +49,9 @@ If set to `true`, allows the pipeline to continue with the execution of the next
 
 * *`values`* _map of strings_ (optional, overridable)
 +
-A key value map, which is made accessible to the template rendering engine as link:{{< relref "overview.adoc#_values" >}}[`Values`] object, e.g. to render parts of the URL.
+A key value map, which is made accessible to the template rendering engine as link:{{< relref "overview.adoc#_values" >}}[`Values`] object to render parts of the URL or the payload.
 
-.Contextualizer configuration
+.Contextualizer configuration without payload
 ====
 
 In this example the contextualizer is configured to call an endpoint using the HTTP `GET` method with the subject id being part of the url path. As the endpoint requires the `X-My-Session-Cookie` cookie for subject authentication purposes, `forward_cookies` is used to achieve this.
@@ -68,3 +68,23 @@ config:
     - X-My-Session-Cookie
 ----
 ====
+
+.Contextualizer configuration with payload
+====
+
+In this example the contextualizer is configured to call an endpoint using the HTTP `POST` and send some data. Since the `values` property is not defined, but used in the payload, it must be specified in a rule making use of this contextualzer.
+
+[source, yaml]
+----
+id: foo
+type: generic
+config:
+  endpoint:
+    url: https://some-other.service/users
+    method: POST
+  payload: |
+    {
+      "user_id": {{ quote .Values.user_id }}
+    }
+----
+====
diff --git a/go.mod b/go.mod
index c8d0129d..4fca498e 100644
--- a/go.mod
+++ b/go.mod
@@ -61,7 +61,7 @@ require (
 	go.uber.org/fx v1.20.0
 	gocloud.dev v0.30.0
 	golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529
 	google.golang.org/grpc v1.56.1
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/square/go-jose.v2 v2.6.0
diff --git a/go.sum b/go.sum
index ce3efc7d..538e5cb7 100644
--- a/go.sum
+++ b/go.sum
@@ -3255,6 +3255,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230626202813-9b080da550b3 h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230626202813-9b080da550b3/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82 h1:6b+zGQBiXFlAMpQr+cCarAdrZD4QgXSG7uUZadYysgg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
diff --git a/internal/rules/mechanisms/authorizers/remote_authorizer.go b/internal/rules/mechanisms/authorizers/remote_authorizer.go
index 08037437..80218884 100644
--- a/internal/rules/mechanisms/authorizers/remote_authorizer.go
+++ b/internal/rules/mechanisms/authorizers/remote_authorizer.go
@@ -289,6 +289,7 @@ func (a *remoteAuthorizer) createRequest(ctx heimdall.Context, sub *subject.Subj
 		bodyContents, err := a.payload.Render(map[string]any{
 			"Request": ctx.Request(),
 			"Subject": sub,
+			"Values":  a.v,
 		})
 		if err != nil {
 			return nil, errorchain.
diff --git a/internal/rules/mechanisms/contextualizers/generic_contextualizer.go b/internal/rules/mechanisms/contextualizers/generic_contextualizer.go
index 70217052..68e83882 100644
--- a/internal/rules/mechanisms/contextualizers/generic_contextualizer.go
+++ b/internal/rules/mechanisms/contextualizers/generic_contextualizer.go
@@ -251,6 +251,7 @@ func (h *genericContextualizer) createRequest(ctx heimdall.Context, sub *subject
 		value, err := h.payload.Render(map[string]any{
 			"Request": ctx.Request(),
 			"Subject": sub,
+			"Values":  h.v,
 		})
 		if err != nil {
 			return nil, errorchain.NewWithMessage(heimdall.ErrInternal,
diff --git a/schema/config.schema.json b/schema/config.schema.json
index a97523eb..b25a4aca 100644
--- a/schema/config.schema.json
+++ b/schema/config.schema.json
@@ -517,13 +517,6 @@
               "description": "Enables or disables http cache usage according to RFC 7234",
               "type": "boolean",
               "default": false
-            },
-            "values": {
-              "description": "Key-Value map with entries required for templating of e.g. the URL",
-              "type": "object",
-              "minLength": 0,
-              "uniqueItems": true,
-              "default": []
             }
           }
         },
@@ -1170,6 +1163,13 @@
                 "1m",
                 "30s"
               ]
+            },
+            "values": {
+              "description": "Key-Value map with entries required for templating of e.g. the endpoint URL",
+              "type": "object",
+              "minLength": 0,
+              "uniqueItems": true,
+              "default": []
             }
           }
         }
@@ -1238,6 +1238,13 @@
               "type": "boolean",
               "description": "Continue the pipeline execution even if this contextualizer fails",
               "default": false
+            },
+            "values": {
+              "description": "Key-Value map with entries required for templating of e.g. the endpoint URL",
+              "type": "object",
+              "minLength": 0,
+              "uniqueItems": true,
+              "default": []
             }
           }
         }


Test patch:
diff --git a/internal/config/test_data/test_config.yaml b/internal/config/test_data/test_config.yaml
index e7eb05eb..4ac92a6f 100644
--- a/internal/config/test_data/test_config.yaml
+++ b/internal/config/test_data/test_config.yaml
@@ -290,8 +290,8 @@ rules:
             method: POST
             headers:
               foo-bar: "{{ .Subject.ID }}"
-            values:
-              key: value
+          values:
+            key: value
           payload: https://bla.bar
           forward_response_headers_to_upstream:
             - bla-bar
@@ -320,14 +320,14 @@ rules:
             url: http://profile/{{ .Subject.ID }}
             headers:
               foo: bar
-            values:
-              some-key: some-value
             auth:
               type: api_key
               config:
                 in: query
                 name: key
                 value: super duper secret
+          values:
+            some-key: some-value
     unifiers:
       - id: jwt
         type: jwt
diff --git a/internal/rules/mechanisms/authorizers/remote_authorizer_test.go b/internal/rules/mechanisms/authorizers/remote_authorizer_test.go
index c498ca81..a30f36ec 100644
--- a/internal/rules/mechanisms/authorizers/remote_authorizer_test.go
+++ b/internal/rules/mechanisms/authorizers/remote_authorizer_test.go
@@ -517,8 +517,9 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
 					URL:     srv.URL,
 					Headers: map[string]string{"Foo-Bar": "{{ .Subject.Attributes.bar }}"},
 				},
+				v: values.Values{"foo": "bar"},
 				payload: func() template.Template {
-					tpl, _ := template.New("{{ .Subject.ID }}")
+					tpl, _ := template.New("{{ .Subject.ID }}-{{ .Values.foo }}")
 
 					return tpl
 				}(),
@@ -543,7 +544,7 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
 					data, err := io.ReadAll(req.Body)
 					assert.NoError(t, err)
 
-					assert.Equal(t, "my-id", string(data))
+					assert.Equal(t, "my-id-bar", string(data))
 				}
 			},
 			configureContext: func(t *testing.T, ctx *heimdallmocks.ContextMock) {
diff --git a/internal/rules/mechanisms/contextualizers/generic_contextualizer_test.go b/internal/rules/mechanisms/contextualizers/generic_contextualizer_test.go
index 1e4597a2..b768dcb9 100644
--- a/internal/rules/mechanisms/contextualizers/generic_contextualizer_test.go
+++ b/internal/rules/mechanisms/contextualizers/generic_contextualizer_test.go
@@ -782,8 +782,14 @@ func TestGenericContextualizerExecute(t *testing.T) {
 						"X-Bar":        "{{ .Subject.Attributes.bar }}",
 					},
 				},
+				v: values.Values{"foo": "bar"},
 				payload: func() template.Template {
-					tpl, _ := template.New(`{ "user_id": {{ quote .Subject.ID }}}`)
+					tpl, _ := template.New(`
+{
+	"user_id": {{ quote .Subject.ID }},
+	"value": {{ quote .Values.foo }}
+}
+`)
 
 					return tpl
 				}(),
@@ -809,7 +815,7 @@ func TestGenericContextualizerExecute(t *testing.T) {
 					content, err := io.ReadAll(req.Body)
 					require.NoError(t, err)
 
-					assert.JSONEq(t, `{"user_id": "Foo"}`, string(content))
+					assert.JSONEq(t, `{"user_id": "Foo", "value": "bar"}`, string(content))
 				}
 
 				responseContentType = "application/json"