Skip to content

Commit

Permalink
feat: Remote authorizer optionally supports verification of responses…
Browse files Browse the repository at this point in the history
… from the remote system via a script (#117)
  • Loading branch information
dadrus authored Jul 28, 2022
1 parent aaea187 commit 1ecabf0
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 28 deletions.
12 changes: 9 additions & 3 deletions docs/content/docs/configuration/pipeline/authorizers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ config:

=== Remote

This authorizer allows communication with other systems, like https://www.openpolicyagent.org/[Open Policy Agent], https://www.ory.sh/docs/keto/[Ory Keto], etc. for the actual authorization purpose. If the used endpoint answers with a not 2xx HTTP response code, this authorizer assumes, the authorization has failed and denies the request. So, the successful execution of the pipeline stops, resulting in the execution of the error handlers. Otherwise, the authorizer assumes, the request has been authorized.
This authorizer allows communication with other systems, like https://www.openpolicyagent.org/[Open Policy Agent], https://www.ory.sh/docs/keto/[Ory Keto], etc. for the actual authorization purpose. If the used endpoint answers with a not 2xx HTTP response code, this authorizer assumes, the authorization has failed and denies the request. So, the successful execution of the pipeline stops, resulting in the execution of the error handlers. Otherwise, if no script for the verification of the response if defined, the authorizer assumes, the request has been authorized. If a script is defined and does not fail, the authorization succeeds.

If your authorization system provides a payload in the response, Heimdall inspects the `Content-Type` header to prepare the payload for further usage, e.g. in a link:{{< relref "#_local" >}}[Local] authorizer. It can however deal only with a content type, which either ends with `json` or which is `application/x-www-form-urlencoded`. In these two cases, the payload is decoded and made available as map in the `.Subject.Attributes` of the subject. Otherwise, the payload is treated as string and made also available in the `.Subject.Attributes` property of the subject. To avoid overwriting of existing attributes, this object is however not available on the top level, but under a key named by the `id` of the authorizer (See also the example below).
If your authorization system provides a payload in the response, Heimdall inspects the `Content-Type` header to prepare the payload for further usage, e.g. in the payload verification script, or in a link:{{< relref "#_local" >}}[Local] authorizer. It can however deal only with a content type, which either ends with `json` or which is `application/x-www-form-urlencoded`. In these two cases, the payload is decoded and made available for the script as well as a map in the `.Subject.Attributes`. Otherwise, the payload is treated as string and made also available for the script and in the `.Subject.Attributes` property. To avoid overwriting of existing attributes, this object is however not available on the top level, but under a key named by the `id` of the authorizer (See also the example below).

To enable the usage of this authorizer, you have to set the `type` property to `remote`.

Expand All @@ -87,10 +87,14 @@ Configuration using the `config` property is mandatory. Following properties are
+
The API endpoint of your authorization system. At least the `url` must be configured. This handler 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.

* *`payload`*: _string_ (mandatory, overridable)
* *`payload`*: _string_ (optional, overridable)
+
Your template with definitions required to communicate to the authorization endpoint. See also link:{{< relref "overview.adoc#_templating" >}}[Templating].

* *`script`*: _string_ (optional, overridable)
+
ECMAScript wich executed further authorization logic on the given response from the authorization endpoint. Heimdall expects the script to return either `true`, if the authorization was successful, or otherwise `false`, or to raise an error. In latter case the message from the raised error will also be logged. Compared to the link:{{< relref "#_local" >}}[Local] authorizer, only `heimdall.Payload` object is available, which contains the response from the authorization endpoint, as well as the `console.log` function, which enables logging from the script. Latter can become handy during development of debugging. The output is only available if debug log level is set.

* *`forward_response_headers_to_upstream`*: _string array_ (optional, overridable)
+
Enables forwarding of any headers from the authorization endpoint response to the upstream service.
Expand Down Expand Up @@ -120,6 +124,8 @@ config:
password: SuperSecretPassword
payload: |
{ "input": { "user": {{ quote .Subject.ID }}, "access": "write" } }
script: |
heimdall.Payload.result === true
----
Since an OPA response could look like `{ "result": true }` or `{ "result": false }`, which obviously needs further evaluation, Heimdall makes it available under `.Subject.Attributes["foo"]` as a map, with `"foo"` being the id of the authorizer in this example.
Expand Down
8 changes: 7 additions & 1 deletion docs/content/docs/configuration/pipeline/overview.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ You can find further examples as part of handler descriptions, supporting templa

== Scripting

Some authorizers, which verify the presence or values of particular attributes of the subject can make use of https://262.ecma-international.org/5.1/[ECMAScript 5.1(+)]. Heimdall uses https://github.com/dop251/goja[goja] as ECMAScript engine. In addition to the general ECMAScript functionality, heimdall makes the same functions and object available to the script, which are also available for the link:{{< relref "#_templating" >}}[templates]. The only difference is, that these are available as part of a `heimdall` object. In addition, there is also a `console` object implementing a `log` function to enable logging from the script. Latter can become handy during development of debugging. The output is only available if `debug` log level is set.
Some authorizers, which verify the presence or values of particular attributes of the subject can make use of https://262.ecma-international.org/5.1/[ECMAScript 5.1(+)]. Heimdall uses https://github.com/dop251/goja[goja] as ECMAScript engine. In addition to the general ECMAScript functionality, heimdall makes the same functions and object available to the script, which are also available for the link:{{< relref "#_templating" >}}[templates]. All scripts can make use of a `console` object implementing a `log` function to enable logging from the script, which can become handy during development or debugging. The output is only available if `debug` log level is set. Beyond this there is also a `heimdall` object available. Depending on the authorizer it contains the `Subject` object and the request context function (already described in link:{{< relref "_templating" >}}[Templating] section), or the `Payload` object, which makes allows access to the response from remote authorization endpoints.

.Script, rendering a JSON object
====
Expand Down Expand Up @@ -237,5 +237,11 @@ The result is again the already known JSON object.
----
====

.Script, checking the response from an OPA endpoint
====
[source, javascript]
heimdall.Payload.result === true
====

You can find further examples as part of handler descriptions, supporting scripting.

Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pipeline:
headers:
foo-bar: "{{ .Subject.ID }}"
payload: "https://bla.bar"
script: "heimdall.Payload.response === true"
forward_response_headers_to_upstream:
- bla-bar
- id: "attributes_based_authorizer"
Expand Down
2 changes: 1 addition & 1 deletion internal/pipeline/authorizers/local_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (a *localAuthorizer) Execute(ctx heimdall.Context, sub *subject.Subject) er
logger := zerolog.Ctx(ctx.AppContext())
logger.Debug().Msg("Authorizing using local authorizer")

res, err := a.s.Execute(ctx, sub)
res, err := a.s.ExecuteOnSubject(ctx, sub)
if err != nil {
return errorchain.New(heimdall.ErrAuthorization).CausedBy(err)
}
Expand Down
46 changes: 36 additions & 10 deletions internal/pipeline/authorizers/remote_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/dadrus/heimdall/internal/pipeline/contenttype"
"github.com/dadrus/heimdall/internal/pipeline/endpoint"
"github.com/dadrus/heimdall/internal/pipeline/renderer"
"github.com/dadrus/heimdall/internal/pipeline/script"
"github.com/dadrus/heimdall/internal/pipeline/subject"
"github.com/dadrus/heimdall/internal/pipeline/template"
"github.com/dadrus/heimdall/internal/x"
Expand All @@ -46,6 +47,7 @@ type remoteAuthorizer struct {
e endpoint.Endpoint
name string
payload template.Template
script script.Script
headersForUpstream []string
ttl time.Duration
}
Expand All @@ -55,7 +57,7 @@ type authorizationInformation struct {
payload any
}

func (ai *authorizationInformation) AddHeadersTo(headerNames []string, ctx heimdall.Context) {
func (ai *authorizationInformation) addHeadersTo(headerNames []string, ctx heimdall.Context) {
for _, headerName := range headerNames {
headerValue := ai.headers.Get(headerName)
if len(headerValue) != 0 {
Expand All @@ -64,7 +66,7 @@ func (ai *authorizationInformation) AddHeadersTo(headerNames []string, ctx heimd
}
}

func (ai *authorizationInformation) AddAttributesTo(key string, sub *subject.Subject) {
func (ai *authorizationInformation) addAttributesTo(key string, sub *subject.Subject) {
if ai.payload != nil {
sub.Attributes[key] = ai.payload
}
Expand All @@ -74,6 +76,7 @@ func newRemoteAuthorizer(name string, rawConfig map[string]any) (*remoteAuthoriz
type Config struct {
Endpoint endpoint.Endpoint `mapstructure:"endpoint"`
Payload template.Template `mapstructure:"payload"`
Script script.Script `mapstructure:"script"`
ResponseHeadersToForward []string `mapstructure:"forward_response_headers_to_upstream"`
CacheTTL time.Duration `mapstructure:"cache_ttl"`
}
Expand All @@ -98,6 +101,7 @@ func newRemoteAuthorizer(name string, rawConfig map[string]any) (*remoteAuthoriz
e: conf.Endpoint,
name: name,
payload: conf.Payload,
script: conf.Script,
headersForUpstream: conf.ResponseHeadersToForward,
ttl: conf.CacheTTL,
}, nil
Expand Down Expand Up @@ -151,8 +155,8 @@ func (a *remoteAuthorizer) Execute(ctx heimdall.Context, sub *subject.Subject) e
}
}

authInfo.AddHeadersTo(a.headersForUpstream, ctx)
authInfo.AddAttributesTo(a.name, sub)
authInfo.addHeadersTo(a.headersForUpstream, ctx)
authInfo.addAttributesTo(a.name, sub)

return nil
}
Expand Down Expand Up @@ -185,10 +189,12 @@ func (a *remoteAuthorizer) doAuthorize(ctx heimdall.Context, sub *subject.Subjec
return nil, err
}

return &authorizationInformation{
headers: resp.Header,
payload: data,
}, nil
err = a.verify(ctx, data)
if err != nil {
return nil, err
}

return &authorizationInformation{headers: resp.Header, payload: data}, nil
}

func (a *remoteAuthorizer) createRequest(ctx heimdall.Context, sub *subject.Subject) (*http.Request, error) {
Expand Down Expand Up @@ -242,8 +248,6 @@ func (a *remoteAuthorizer) readResponse(ctx heimdall.Context, resp *http.Respons

contentType := resp.Header.Get("Content-Type")

logger.Debug().Msgf("Received response of %s content type", contentType)

decoder, err := contenttype.NewDecoder(contentType)
if err != nil {
logger.Warn().Msgf("%s content type is not supported. Treating it as string", contentType)
Expand All @@ -267,6 +271,7 @@ func (a *remoteAuthorizer) WithConfig(rawConfig map[string]any) (Authorizer, err

type Config struct {
Payload template.Template `mapstructure:"payload"`
Script script.Script `mapstructure:"script"`
ResponseHeadersToForward []string `mapstructure:"forward_response_headers_to_upstream"`
CacheTTL time.Duration `mapstructure:"cache_ttl"`
}
Expand All @@ -281,6 +286,7 @@ func (a *remoteAuthorizer) WithConfig(rawConfig map[string]any) (Authorizer, err
e: a.e,
name: a.name,
payload: x.IfThenElse(conf.Payload != nil, conf.Payload, a.payload),
script: x.IfThenElse(conf.Script != nil, conf.Script, a.script),
headersForUpstream: x.IfThenElse(len(conf.ResponseHeadersToForward) != 0,
conf.ResponseHeadersToForward, a.headersForUpstream),
ttl: x.IfThenElse(conf.CacheTTL > 0, conf.CacheTTL, a.ttl),
Expand Down Expand Up @@ -311,3 +317,23 @@ func (a *remoteAuthorizer) calculateCacheKey(sub *subject.Subject) (string, erro

return hex.EncodeToString(hash.Sum(nil)), nil
}

func (a *remoteAuthorizer) verify(ctx heimdall.Context, result any) error {
if a.script == nil {
return nil
}

logger := zerolog.Ctx(ctx.AppContext())
logger.Debug().Msg("Verifying authorization response using script")

res, err := a.script.ExecuteOnPayload(ctx, result)
if err != nil {
return errorchain.New(heimdall.ErrAuthorization).CausedBy(err)
}

if !res.ToBoolean() {
return errorchain.NewWithMessage(heimdall.ErrAuthorization, "script failed")
}

return nil
}
Loading

0 comments on commit 1ecabf0

Please sign in to comment.