Skip to content

Commit

Permalink
refactor!: Objects and functions available in templates and CEL expre…
Browse files Browse the repository at this point in the history
…ssions harmonized (#394)
  • Loading branch information
dadrus authored Dec 23, 2022
1 parent d218e38 commit 4ca9a9d
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ Configuration using the `config` property is mandatory. Following properties are

* *`expressions`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_authorization_expression">}}[Authorization Expression] array_ (mandatory, overridable)
+
List of authorization expressions, which define the actual authorization logic. Each expression has access to the following link:{{< relref "overview.adoc#_evaluation_objects" >}}[Evaluation Objects]:

** `Subject` of type link:{{< relref "overview.adoc#_subject" >}}[Subject].
** `Request` of type link:{{< relref "overview.adoc#_request" >}}[Request]
List of authorization expressions, which define the actual authorization logic. Each expression has access to the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] and the link:{{< relref "overview.adoc#_request" >}}[`Request`] objects.

.Authorization based on subject properties
====
Expand Down Expand Up @@ -115,19 +112,17 @@ Configuration using the `config` property is mandatory. Following properties are

* *`endpoint`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_endpoint">}}[Endpoint]_ (mandatory, not overridable)
+
The API endpoint of your authorization system. At least the `url` must be configured. This mechanism allows templating of the url and makes the `Subject` object available to it. By default, this authorizer will use HTTP `POST` to send the rendered payload to this endpoint. You can override this behavior by configuring `method` as well. Depending on the API requirements of your authorization system, you might need to configure further properties, like headers, etc.
The API endpoint of your authorization system. At least the `url` must be configured. This mechanism allows templating of the url and makes the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] object available to it. By default, this authorizer will use HTTP `POST` to send the rendered payload to this endpoint. You can override this behavior by configuring `method` as well. Depending on the API requirements of your authorization system, you might need to configure further properties, like headers, etc.

* *`payload`*: _string_ (optional, overridable)
+
Your template with definitions required to communicate to the authorization endpoint. See also link:{{< relref "overview.adoc#_templating" >}}[Templating].
Your link:{{< relref "overview.adoc#_templating" >}}[template] with definitions required to communicate to the authorization endpoint. The template can make use of link:{{< relref "overview.adoc#_subject" >}}[`Subject`] and link:{{< relref "overview.adoc#_request" >}}[`Request`] objects.

* *`expressions`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_authorization_expression">}}[Authorization Expression] array_ (optional, overridable)
+
List of https://github.com/google/cel-spec[CEL] expressions which define the logic to be applied to the response returned by the endpoint. All expressions are expected to evaluate to `true` if the authorization was successful. If any of the expressions evaluates to `false`, the authorization fails and the message defined by the failed expression will be logged.
+
Each expression has access to the following link:{{< relref "overview.adoc#_evaluation_objects" >}}[Evaluation Objects]:

** `Payload` of type link:{{< relref "overview.adoc#_payload" >}}[Payload].
Each expression has access to the link:{{< relref "overview.adoc#_payload" >}}[`Payload`] object.

* *`forward_response_headers_to_upstream`*: _string array_ (optional, overridable)
+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Configuration using the `config` property is mandatory. Following properties are

* *`endpoint`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_endpoint">}}[Endpoint]_ (mandatory, not overridable)
+
The API of the service providing additional attributes about the authenticated user. At least the `url` must be configured. This mechanism allows templating of the url and makes the `Subject` object available to it. By default, this authorizer type uses HTTP `POST` to send the rendered payload to the endpoint. You can however override this behavior by configuring `method`. Depending on the API requirements you might need to configure further properties, like headers, etc as well.
The API of the service providing additional attributes about the authenticated user. At least the `url` must be configured. This mechanism allows templating of the url and makes the link:{{< relref "overview.adoc#_subject" >}}[`Subject`] object available to it. By default, this authorizer type uses HTTP `POST` to send the rendered payload to the endpoint. You can however override this behavior by configuring `method`. Depending on the API requirements you might need to configure further properties, like headers, etc as well.

* *`forward_headers`*: _string array_ (optional, overridable)
+
Expand All @@ -37,7 +37,7 @@ If the API requires any cookies from the request to heimdall, you can forward th

* *`payload`*: _string_ (optional, overridable)
+
Your template with definitions required to communicate to the API. See also link:{{< relref "overview.adoc#_templating" >}}[Templating].
Your link:{{< relref "overview.adoc#_templating" >}}[template] with definitions required to communicate to the endpoint. The template can make use of link:{{< relref "overview.adoc#_subject" >}}[`Subject`] and link:{{< relref "overview.adoc#_request" >}}[`Request`] objects.

* *`cache_ttl`*: _link:{{< relref "/docs/configuration/reference/types.adoc#_duration" >}}[Duration]_ (optional, overridable)
+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,38 +280,35 @@ Payload = "SomeStringValue"

== Templating

Some pipeline mechanisms support templating using https://golang.org/pkg/text/template/[Golang Text Templates]. To ease the usage, all http://masterminds.github.io/sprig/[sprig] functions as well as a `urlenc` function are available. Latter is handy if you need to generate request body or query parameters e.g. for communication with further systems. In addition to the link:{{< relref "#_subject" >}}[Subject] object described above, following functions are available to a template as well:

* `RequestMethod` - function, providing access to the used HTTP method for the given request. Returns a `string`.
* `RequestURL` - function, providing access to the matched URL of the given request. Returns a URL object as defined by https://pkg.go.dev/net/url#URL[Golang net.url.URL]. This way access to properties, like `Scheme`, `Host`, `Path` and other URL properties is easily possible. If used as is it is converted to a `string`.
* `RequestClientIPs` - function, providing information about the client IPs known about the request. Returns a `string array`.
* `RequestHeader` - function, expecting the name of a header as input. Returns the value of the header as `string` if present in the HTTP request. If not present an empty string (`""`) is returned.
* `RequestCookie` - function, expecting the name of a cookie as input. Returns the value of the cookie as `string` if present in the HTTP request. If not present an empty string (`""`) is returned.
* `RequestQueryParameter` - function, expecting the name of a query parameter as input. Returns the value of the query parameter as `string` if present in the HTTP request. If not present an empty string (`""`) is returned.
Some pipeline mechanisms support templating using https://golang.org/pkg/text/template/[Golang Text Templates]. To ease the usage, all http://masterminds.github.io/sprig/[sprig] functions, except `env` and `expandenv`, as well as an `urlenc` function are available. Latter is handy if you need to generate request body or query parameters e.g. for communication with further systems. Templates can act on all objects described above (link:{{< relref "#_subject" >}}[Subject], link:{{< relref "#_request" >}}[Request] and link:{{< relref "#_payload" >}}[Payload]). Which exactly are supported is mechanism specific.

.Template, rendering a JSON object
====
Imagine, we have a `POST` request for the URL `\http://foobar.baz/zab`, with a header `X-Foo` set to `bar` value, for which heimdall was able to identify a subject, with `ID=foo` and which `Attributes` contain an entry `email: foo@bar`, then you can generate a JSON object with this information with the following template:
Imagine, we have a `POST` request for the URL `\http://foobar.baz/zab?foo=bar`, with a header `X-Foo` set to `bar` value, for which heimdall was able to identify a subject, with `ID=foo` and which `Attributes` contain an entry `email: foo@bar`, then you can generate a JSON object with this information with the following template:
[source, gotemplate]
----
{
"subject_id": {{ quote .Subject.ID }},
"email": {{ quote .Subject.Attributes.email }},
"request_url": {{ quote .RequestURL }},
"request_method": {{ quote .RequestMethod }},
"x_foo_value": {{ .RequestHeader "X-Foo" | quote }}
"request_url": {{ quote .Request.URL }},
"foo_value": {{ index .Request.URL.Query.foo 0 | quote }}
"request_method": {{ quote .Request.Method }},
"x_foo_value": {{ .Request.Header "X-Foo" | quote }}
}
----
Please note how the access to the `foo` query parameter is done. Since `.Request.URL.Query.foo` returns an array of strings, the first element is taken to render the value for the `foo_value` key.
This will result in the following JSON object:
[source, json]
----
{
"subject_id": "foo",
"email": "foo@bar.baz",
"request_url": "http://foobar.baz/zab",
"request_url": "http://foobar.baz/zab?foo=bar",
"foo_value": "bar",
"request_method": "POST",
"x_foo_value": "bar"
}
Expand Down Expand Up @@ -342,6 +339,13 @@ a CEL expression to check the `result` attribute is set to `true`, would look as
----
Payload.result == true
----
or even simpler:
[source, cel]
----
Payload.result
----
====

.Check whether the user is member of the admin group
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,7 @@ config:
{{ $user_name := .Subject.Attributes.identity.user_name -}}
"email": {{ quote .Subject.Attributes.identity.email }},
"email_verified": {{ .Subject.Attributes.identity.email_verified }},
{{ if $user_name -}}
"name": {{ quote $user_name }}
{{ else -}}
"name": {{ quote $email }}
{{ end -}}
"name": {{ if $user_name }}{{ quote $user_name }}{{ else }}{{ quote $email }}{{ end }}
}
----
====
37 changes: 37 additions & 0 deletions internal/rules/mechanisms/authorizers/remote_authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
assert.Equal(t, "my-id", string(data))
}
},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()

Expand Down Expand Up @@ -530,6 +537,9 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
t.Helper()

ctx.On("AddHeaderForUpstream", "X-Foo-Bar", "HeyFoo")
ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()
Expand Down Expand Up @@ -603,6 +613,9 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
t.Helper()

ctx.On("AddHeaderForUpstream", "X-Foo-Bar", "HeyFoo")
ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
configureCache: func(t *testing.T, cch *mocks.MockCache, auth *remoteAuthorizer, sub *subject.Subject) {
t.Helper()
Expand Down Expand Up @@ -758,6 +771,9 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
t.Helper()

ctx.On("AddHeaderForUpstream", "X-Foo-Bar", "HeyFoo")
ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
configureCache: func(t *testing.T, cch *mocks.MockCache, auth *remoteAuthorizer, sub *subject.Subject) {
t.Helper()
Expand Down Expand Up @@ -861,6 +877,13 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
}(),
},
subject: &subject.Subject{ID: "foo"},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()

Expand Down Expand Up @@ -953,6 +976,13 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
responseContent = rawData
responseContentType = "application/json"
},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()

Expand Down Expand Up @@ -1028,6 +1058,13 @@ func TestRemoteAuthorizerExecute(t *testing.T) {
responseContent = rawData
responseContentType = "application/json"
},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -490,6 +491,13 @@ func TestGenericContextualizerExecute(t *testing.T) {
return val != nil && val.payload == "Hi from endpoint"
}), 5*time.Second)
},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
instructServer: func(t *testing.T) {
t.Helper()

Expand Down Expand Up @@ -519,6 +527,13 @@ func TestGenericContextualizerExecute(t *testing.T) {
}(),
},
subject: &subject.Subject{ID: "Foo", Attributes: map[string]any{"bar": "baz"}},
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()

Expand Down Expand Up @@ -699,10 +714,12 @@ func TestGenericContextualizerExecute(t *testing.T) {
configureContext: func(t *testing.T, ctx *heimdallmocks.MockContext) {
t.Helper()

ctx.On("RequestHeader", "X-Bar-Foo").
Return("Hi Foo")
ctx.On("RequestHeader", "X-Bar-Foo").Return("Hi Foo")
ctx.On("RequestCookie", "X-Foo-Session").
Return("Foo-Session-Value")
ctx.On("RequestMethod").Return("POST")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestClientIPs").Return(nil)
},
assert: func(t *testing.T, err error, sub *subject.Subject) {
t.Helper()
Expand Down
46 changes: 14 additions & 32 deletions internal/rules/mechanisms/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ type templateImpl struct {
}

func New(val string) (Template, error) {
funcMap := sprig.TxtFuncMap()
delete(funcMap, "env")
delete(funcMap, "expandenv")

tmpl, err := template.New("Heimdall").
Funcs(sprig.TxtFuncMap()).
Funcs(funcMap).
Funcs(template.FuncMap{"urlenc": url.QueryEscape}).
Parse(val)
if err != nil {
Expand All @@ -43,9 +47,16 @@ func New(val string) (Template, error) {
}

func (t *templateImpl) Render(ctx heimdall.Context, sub *subject.Subject) (string, error) {
var buf bytes.Buffer
var (
buf bytes.Buffer
req *Request
)

if ctx != nil {
req = WrapRequest(ctx)
}

err := t.t.Execute(&buf, data{Subject: sub, ctx: ctx})
err := t.t.Execute(&buf, data{Subject: sub, Request: req})
if err != nil {
return "", errorchain.New(ErrTemplateRender).CausedBy(err)
}
Expand All @@ -54,32 +65,3 @@ func (t *templateImpl) Render(ctx heimdall.Context, sub *subject.Subject) (strin
}

func (t *templateImpl) Hash() []byte { return t.hash }

type data struct {
ctx heimdall.Context
Subject *subject.Subject
}

func (t data) RequestMethod() string {
return t.ctx.RequestMethod()
}

func (t data) RequestURL() string {
return t.ctx.RequestURL().String()
}

func (t data) RequestClientIPs() []string {
return t.ctx.RequestClientIPs()
}

func (t data) RequestHeader(name string) string {
return t.ctx.RequestHeader(name)
}

func (t data) RequestCookie(name string) string {
return t.ctx.RequestCookie(name)
}

func (t data) RequestQueryParameter(name string) string {
return t.ctx.RequestQueryParameter(name)
}
24 changes: 9 additions & 15 deletions internal/rules/mechanisms/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,12 @@ func TestTemplateRender(t *testing.T) {
t.Parallel()

// GIVEN

ctx := &mocks.MockContext{}
ctx.On("RequestMethod").Return("PATCH")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestHeaders").Return(map[string]string{
"Accept": "application/json",
"X-My-Header": "my-value",
})
ctx.On("RequestHeader", "X-My-Header").Return("my-value")
ctx.On("RequestCookie", "session_cookie").Return("session-value")
ctx.On("RequestQueryParameter", "my_query_param").Return("query_value")
ctx.On("RequestURL").Return(&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab"})
ctx.On("RequestURL").Return(
&url.URL{Scheme: "http", Host: "foobar.baz", Path: "zab", RawQuery: "my_query_param=query_value"})
ctx.On("RequestClientIPs").Return([]string{"192.168.1.1"})

sub := &subject.Subject{
Expand All @@ -44,12 +38,12 @@ func TestTemplateRender(t *testing.T) {
"name": {{ quote .Subject.Attributes.name }},
"email": {{ quote .Subject.Attributes.email }},
"complex": "{{ range $i, $el := .Subject.Attributes.complex -}}{{ if $i }} {{ end }}{{ $el }}{{ end }}",
"request_url": {{ quote .RequestURL }},
"request_method": {{ quote .RequestMethod }},
"my_header": {{ .RequestHeader "X-My-Header" | quote }},
"my_cookie": {{ .RequestCookie "session_cookie" | quote }},
"my_query_param": {{ .RequestQueryParameter "my_query_param" | quote }},
"ips": "{{ range $i, $el := .RequestClientIPs -}}{{ if $i }} {{ end }}{{ $el }}{{ end }}"
"request_url": {{ quote .Request.URL }},
"request_method": {{ quote .Request.Method }},
"my_header": {{ .Request.Header "X-My-Header" | quote }},
"my_cookie": {{ .Request.Cookie "session_cookie" | quote }},
"my_query_param": {{ index .Request.URL.Query.my_query_param 0 | quote }},
"ips": "{{ range $i, $el := .Request.ClientIP -}}{{ if $i }} {{ end }}{{ $el }}{{ end }}"
}`)
require.NoError(t, err)

Expand All @@ -64,7 +58,7 @@ func TestTemplateRender(t *testing.T) {
"name": "bar",
"email": "foo@bar.baz",
"complex": "test1 test2",
"request_url": "http://foobar.baz/zab",
"request_url": "http://foobar.baz/zab?my_query_param=query_value",
"request_method": "PATCH",
"my_header": "my-value",
"my_cookie": "session-value",
Expand Down
Loading

0 comments on commit 4ca9a9d

Please sign in to comment.