Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: Made the usage of if clauses in authentication & authorization, and error pipelines consistent #1784

Merged
merged 20 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a5ecac4
obsolete base error handler deleted as its functionality is going to …
dadrus Sep 12, 2024
591d674
conditional error handler decorator implementation
dadrus Sep 12, 2024
90dc5c2
schema updated - if clause removed from error handler definitions
dadrus Sep 12, 2024
0302593
errorHandler interface updated - CanExecuted removed
dadrus Sep 12, 2024
968ee11
executionCondition interface updated to define two methods, one for t…
dadrus Sep 12, 2024
318cacd
CEL based execution condition implementation updated to implement the…
dadrus Sep 12, 2024
a5e1751
default execution condition implementation updated to support the new…
dadrus Sep 12, 2024
83095cb
conditional subject handler updated to implement the new method
dadrus Sep 12, 2024
4e9dcf7
composite error handler updated
dadrus Sep 12, 2024
4a7c52d
mocks regenerated
dadrus Sep 12, 2024
56b9180
rule implementation test updated
dadrus Sep 12, 2024
55a19df
rule factory implementation updated to decorate error handlers with c…
dadrus Sep 12, 2024
fae7257
error handler implementations updated; those who do not support recon…
dadrus Sep 12, 2024
8004a3b
test configs used for validation tests updated
dadrus Sep 12, 2024
0395080
example config updated
dadrus Sep 12, 2024
9110f96
documentation updated
dadrus Sep 12, 2024
936b17b
kubernetes quickstart examples updated
dadrus Sep 12, 2024
babcdd8
linter warnings resolved
dadrus Sep 13, 2024
20752aa
some dead code removed simplifications and more tests
dadrus Sep 13, 2024
c240707
linter warning resolved
dadrus Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/validate/test_data/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,6 @@ mechanisms:
type: default
- id: authenticate_with_kratos
type: redirect
if: |
((type(Error) == authentication_error && Error.Source == "kratos_session_authenticator") ||
type(Error) == authorization_error) &&
Request.Header("Accept").contains("*/*")
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}

Expand All @@ -182,6 +178,10 @@ default_rule:
- finalizer: jwt
on_error:
- error_handler: authenticate_with_kratos
if: |
((type(Error) == authentication_error && Error.Source == "kratos_session_authenticator") ||
type(Error) == authorization_error) &&
Request.Header("Accept").contains("*/*")

providers:
file_system:
Expand Down
12 changes: 4 additions & 8 deletions docs/content/docs/configuration/reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,6 @@ mechanisms:
type: redirect
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}
when:
- error:
- type: authentication_error
raised_by: kratos_session_authenticator
- type: authorization_error
request_headers:
Accept:
- '*/*'

default_rule:
backtracking_enabled: false
Expand All @@ -373,6 +365,10 @@ default_rule:
- finalizer: jwt
on_error:
- error_handler: authenticate_with_kratos
if: |
((type(Error) == authentication_error && Error.Source == "kratos_session_authenticator") ||
type(Error) == authorization_error) &&
Request.Header("Accept").contains("*/*")

providers:
file_system:
Expand Down
3 changes: 0 additions & 3 deletions docs/content/docs/mechanisms/catalogue.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,6 @@ mechanisms:
- id: default
type: default
- id: authenticate_with_kratos
if: |
type(Error) in [authentication_error, authorization_error] &&
Request.Header("Accept").contains("text/html")
type: redirect
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}
Expand Down
7 changes: 2 additions & 5 deletions docs/content/docs/mechanisms/error_handlers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Some of the error handlers may support or require additional configuration. The

== Default

This mechanism is always there and is executed if no other error handler mechanism is responsible for the error. Actually, there is no need to explicitly configure it. The only exception is to allow overriding the link:{{< relref "/docs/rules/default_rule.adoc" >}}[default rule's] error handler chain in a specific rule for performance reasons (if configured error handlers in the default rule should not be considered). This mechanism type doesn't have any configuration options.
This error handler is always there and is executed if no other error handler mechanism is responsible for the error. Actually, there is no need to explicitly configure it. The only exception is to allow overriding the link:{{< relref "/docs/rules/default_rule.adoc" >}}[default rule's] error handler chain in a specific rule for performance reasons (if configured error handlers in the default rule should not be considered). This mechanism type doesn't have any configuration options.

To enable the usage of this mechanism, you have to set the `type` property to `default`.

Expand All @@ -37,7 +37,7 @@ This error handler mechanism allows redirecting the client to another endpoint,

To enable the usage of this mechanism, you have to set the `type` property to `redirect`.

Configuration is mandatory by making use of the `if` and `config` properties. The first defines the condition, which must hold true for this error handler to execute and has access to the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_request" >}}[`Request`] and the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_error" >}}[`Error`] objects. Latter defines the data to drive the redirect and supports the following properties:
Configuration is mandatory by making use of the `config` property supporting the following settings:

* *`to`*: _URL_ (mandatory, not overridable)
+
Expand All @@ -62,9 +62,6 @@ So, e.g. if heimdall was handling the request for `\http://my-service.local/foo`
----
id: authenticate_with_kratos
type: redirect
if: |
type(Error) in [authentication_error, authorization_error] &&
Request.Header("Accept").contains("text/html")
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}
----
Expand Down
6 changes: 4 additions & 2 deletions docs/content/docs/rules/default_rule.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Enables or disables backtracking while matching the rules globally. Defaults to
+
Which mechanisms to use for authentication, authorization and finalization stages of the pipeline. At least the authentication stage with at least one link:{{< relref "/docs/mechanisms/authenticators.adoc" >}}[authenticator] must be defined. A specific rule (see also link:{{< relref "regular_rule.adoc" >}}[Regular Rule]) can omit the definition of that stage, if it wants to reuse it from in the default rule. Same is true for other stages (See also link:{{< relref "/docs/concepts/rules.adoc#_default_rule_inheritance" >}}[Rule Inheritance]).

* *`on_error`*: _link:{{< relref "regular_rule.adoc#_error_pipeline" >}}[Error Pipeline]_ (mandatory)
* *`on_error`*: _link:{{< relref "regular_rule.adoc#_error_pipeline" >}}[Error Pipeline]_ (optional)
+
Which error handler mechanisms to use if any of the mechanisms, defined in the `execute` property fail. Allows omitting the definition of error handlers in specific rules. As soon as a specific rule defines at least one error handler mechanism, all error handler mechanisms, defined in the default rule are ignored.
Which error handler mechanisms to use if any of the mechanisms, defined in the `execute` property fail. Allows omitting the definition of error handlers in specific rules. As soon as a specific rule defines at least one error handler mechanism, all error handler mechanisms, defined in the default rule are ignored. If not specified, the default error handler is used.

.Default rule configuration
====
Expand All @@ -48,6 +48,8 @@ default_rule:
- finalizer: create_jwt
on_error:
- error_handler: authenticate_with_kratos_eh
if: |
type(Error) == authentication_error && Error.Source == "session_cookie_from_kratos_authn"
----

This example defines a default rule, with the authentication 6 authorization pipeline consisting of two authenticators, with `session_cookie_from_kratos_authn` being the first and `oauth2_introspect_token_from_keycloak_authn` being the fallback one (if the first one fails), a `deny_all_requests_authz` authorizer and the `create_jwt` finalizer. The error pipeline is configured to execute only the `authenticate_with_kratos_eh` error handler.
Expand Down
12 changes: 7 additions & 5 deletions docs/content/docs/rules/regular_rule.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Specifies the mechanisms used for authentication, authorization, contextualizati

* *`on_error`*: _link:{{< relref "#_error_pipeline" >}}[Error Pipeline]_ (optional)
+
Specifies error handling mechanisms if the pipeline defined by the `execute` property fails. Optional, if a link:{{< relref "default_rule.adoc" >}}[default rule] is configured with `on_error` definition.
Specifies error handling mechanisms if the pipeline defined by the `execute` property fails. Defaults to the error pipeline defined in the link:{{< relref "default_rule.adoc" >}}[default rule] if not specified.

.An example rule
====
Expand Down Expand Up @@ -365,21 +365,23 @@ This example uses

== Error Pipeline

Compared to the link:{{< relref "#_authentication_authorization_pipeline" >}}[Authentication & Authorization Pipeline], the error pipeline is pretty simple. It is also a list of mechanism references, but all referenced types are link:{{< relref "/docs/mechanisms/error_handlers.adoc" >}}[error handler types]. Thus, each entry in this list must have `error_handler` as key, followed by the `ìd` of the required error handler, previously defined in the link:{{< relref "/docs/mechanisms/catalogue.adoc" >}}[mechanism catalogue]. Error handlers are always executed as fallbacks. So, if the condition of the first error handler does not match, second is selected, if its condition matches, it is executed, otherwise the next one is selected, etc. If none of the conditions of the defined error handlers match, the link:{{< relref "/docs/mechanisms/error_handlers.adoc#_default" >}}[default error handler] is executed.
Compared to the link:{{< relref "#_authentication_authorization_pipeline" >}}[Authentication & Authorization Pipeline], the error pipeline is pretty simple. It is also a list of mechanism references, but all referenced types are link:{{< relref "/docs/mechanisms/error_handlers.adoc" >}}[error handler types]. Thus, each entry in this list must have `error_handler` as key, followed by the `ìd` of the required error handler previously defined in the link:{{< relref "/docs/mechanisms/catalogue.adoc" >}}[mechanism catalogue].

As with the authentication & authorization pipeline, partial reconfiguration of the used mechanisms is possible if supported by the corresponding type. Same is true for overrides of the `if` conditions. The overrides are always local to the given rule as well.
Execution of the error handlers should happen conditionally by making use of a https://github.com/google/cel-spec[CEL] expression in an `if` clause, which has access to the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_error" >}}[`Error`] and the link:{{< relref "/docs/mechanisms/evaluation_objects.adoc#_request" >}}[`Request`] objects. Otherwise, the first error handler will be executed and the error pipeline will exit.

As with the authentication & authorization pipeline, partial reconfiguration of the used mechanisms is possible if supported by the corresponding type. The overrides are always local to the given rule as well.

.Two error handlers
====
[source, yaml]
----
- error_handler: foo
- error_handler: bar
if: # rule specific condition
- error_handler: bar
config:
# rule specific config
----
====

This example uses two error handlers, named `foo` and `bar`. `bar` will only be selected by heimdall if `foo` 's error condition (defined in the link:{{< relref "/docs/mechanisms/catalogue.adoc" >}}[mechanism catalogue]) does not match. `bar` does also override the error condition as required by the given rule.
This example uses two error handlers, named `foo` and `bar`. `bar` will only be executed if `foo` 's error condition does not match. `bar` does also override the error handler configuration as required by the given rule.

8 changes: 4 additions & 4 deletions example_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,6 @@ mechanisms:
type: default
- id: authenticate_with_kratos
type: redirect
if: |
Error.Source == "kratos_session_authenticator" &&
type(Error) == authentication_error &&
Request.Header("Accept").contains("*/*")
config:
to: http://127.0.0.1:4433/self-service/login/browser?origin={{ .Request.URL | urlenc }}

Expand All @@ -183,6 +179,10 @@ default_rule:
- finalizer: jwt
on_error:
- error_handler: authenticate_with_kratos
if: |
Error.Source == "kratos_session_authenticator" &&
type(Error) == authentication_error &&
Request.Header("Accept").contains("*/*")

providers:
file_system:
Expand Down
4 changes: 2 additions & 2 deletions examples/kubernetes/quickstarts/heimdall/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ mechanisms:
error_handlers:
- id: redirect
type: redirect
if: type(Error) == authentication_error
config:
to: http://foo.bar?origin={{ .Request.URL | urlenc }}

Expand All @@ -40,7 +39,8 @@ default_rule:
- authenticator: anonymous_authenticator
- authorizer: deny_all_requests
on_error:
- error_handler: redirect
- if: type(Error) == authentication_error
error_handler: redirect

providers:
kubernetes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ data:
error_handlers:
- id: redirect
type: redirect
if: type(Error) == authentication_error
config:
to: http://foo.bar?origin={{ .Request.URL | urlenc }}

Expand All @@ -47,6 +46,7 @@ data:
- authorizer: deny_all_requests
on_error:
- error_handler: redirect
if: type(Error) == authentication_error

providers:
file_system:
Expand Down
5 changes: 1 addition & 4 deletions internal/config/test_data/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,6 @@ mechanisms:
type: default
- id: authenticate_with_kratos
type: redirect
if: |
((type(Error) == authentication_error && Error.Source == "kratos_session_authenticator") ||
type(Error) == authorization_error) &&
Request.Header("Accept").contains("*/*")
config:
to: http://127.0.0.1:4433/self-service/login/browser?return_to={{ .Request.URL | urlenc }}

Expand All @@ -481,6 +477,7 @@ default_rule:
- finalizer: jwt
on_error:
- error_handler: authenticate_with_kratos
if: type(Error) == authentication_error

providers:
file_system:
Expand Down
16 changes: 11 additions & 5 deletions internal/rules/cel_execution_condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ type celExecutionCondition struct {
e *cellib.CompiledExpression
}

func (c *celExecutionCondition) CanExecute(ctx heimdall.Context, sub *subject.Subject) (bool, error) {
obj := map[string]any{"Request": ctx.Request()}
func (c *celExecutionCondition) CanExecuteOnSubject(ctx heimdall.Context, sub *subject.Subject) (bool, error) {
if err := c.e.Eval(map[string]any{"Request": ctx.Request(), "Subject": sub}); err != nil {
if errors.Is(err, &cellib.EvalError{}) {
return false, nil
}

if sub != nil {
obj["Subject"] = sub
return false, err
}

if err := c.e.Eval(obj); err != nil {
return true, nil
}

func (c *celExecutionCondition) CanExecuteOnError(ctx heimdall.Context, cause error) (bool, error) {
if err := c.e.Eval(map[string]any{"Request": ctx.Request(), "Error": cellib.WrapError(cause)}); err != nil {
if errors.Is(err, &cellib.EvalError{}) {
return false, nil
}
Expand Down
65 changes: 63 additions & 2 deletions internal/rules/cel_execution_condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/dadrus/heimdall/internal/heimdall"
"github.com/dadrus/heimdall/internal/heimdall/mocks"
"github.com/dadrus/heimdall/internal/rules/mechanisms/subject"
"github.com/dadrus/heimdall/internal/x/errorchain"
)

func TestNewCelExecutionCondition(t *testing.T) {
Expand Down Expand Up @@ -58,7 +59,7 @@ func TestNewCelExecutionCondition(t *testing.T) {
}
}

func TestCelExecutionConditionCanExecute(t *testing.T) {
func TestCelExecutionConditionCanExecuteOnSubject(t *testing.T) {
t.Parallel()

sub := &subject.Subject{
Expand Down Expand Up @@ -117,7 +118,67 @@ func TestCelExecutionConditionCanExecute(t *testing.T) {
require.NoError(t, err)

// WHEN
can, err := condition.CanExecute(ctx, sub)
can, err := condition.CanExecuteOnSubject(ctx, sub)

// THEN
require.NoError(t, err)
assert.Equal(t, tc.expected, can)
})
}
}

type testIdentifier string

func (tid testIdentifier) ID() string { return string(tid) }

func TestCelExecutionConditionCanExecuteOnError(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
uc string
expression string
expected bool
}{
{
uc: "complex expression evaluating to true",
expression: `type(Error) in [communication_error, authorization_error] &&
Error.Source == "foobar" &&
"bar" in Request.URL.Query().foo`,
expected: true,
},
{
uc: "simple expression evaluating to false",
expression: `type(Error) == internal_error && Request.Method == "GET"`,
expected: false,
},
{
uc: "simple expression evaluating to true",
expression: `type(Error) == authorization_error && Request.Method == "GET"`,
expected: true,
},
} {
t.Run(tc.uc, func(t *testing.T) {
// GIVEN
ctx := mocks.NewContextMock(t)

ctx.EXPECT().Request().Return(&heimdall.Request{
Method: http.MethodGet,
URL: &heimdall.URL{URL: url.URL{
Scheme: "http",
Host: "localhost",
Path: "/test",
RawQuery: "foo=bar&baz=zab",
}},
ClientIPAddresses: []string{"127.0.0.1", "10.10.10.10"},
})

condition, err := newCelExecutionCondition(tc.expression)
require.NoError(t, err)

// WHEN
can, err := condition.CanExecuteOnError(ctx, errorchain.
NewWithMessage(heimdall.ErrCommunication, "test").
CausedBy(heimdall.ErrAuthorization).WithErrorContext(testIdentifier("foobar")))

// THEN
require.NoError(t, err)
Expand Down
17 changes: 12 additions & 5 deletions internal/rules/composite_error_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package rules

import (
"errors"

"github.com/rs/zerolog"

"github.com/dadrus/heimdall/internal/heimdall"
Expand All @@ -28,16 +30,21 @@ func (eh compositeErrorHandler) Execute(ctx heimdall.Context, exErr error) error
logger := zerolog.Ctx(ctx.AppContext())
logger.Debug().Msg("Handling pipeline error")

for _, eh := range eh {
if eh.CanExecute(ctx, exErr) {
err := eh.Execute(ctx, exErr)
if err != nil {
logger.Error().Err(err).Msg("Failed to execute error handler")
for _, handler := range eh {
if err := handler.Execute(ctx, exErr); err != nil {
if errors.Is(err, errErrorHandlerNotApplicable) {
continue
}

logger.Error().Err(err).Msg("Failed to execute error handler")

return err
}

return nil
}

logger.Warn().Msg("No applicable error handler found")

return exErr
}
8 changes: 3 additions & 5 deletions internal/rules/composite_error_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ func TestCompositeErrorHandlerExecutionWithFallback(t *testing.T) {
ctx.EXPECT().AppContext().Return(context.Background())

eh1 := rulemocks.NewErrorHandlerMock(t)
eh1.EXPECT().CanExecute(ctx, testsupport.ErrTestPurpose).Return(false)
eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(errErrorHandlerNotApplicable)

eh2 := rulemocks.NewErrorHandlerMock(t)
eh2.EXPECT().CanExecute(ctx, testsupport.ErrTestPurpose).Return(true)
eh2.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(nil)

eh := compositeErrorHandler{eh1, eh2}
Expand All @@ -58,7 +57,6 @@ func TestCompositeErrorHandlerExecutionWithoutFallback(t *testing.T) {
ctx.EXPECT().AppContext().Return(context.Background())

eh1 := rulemocks.NewErrorHandlerMock(t)
eh1.EXPECT().CanExecute(ctx, testsupport.ErrTestPurpose).Return(true)
eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(nil)

eh2 := rulemocks.NewErrorHandlerMock(t)
Expand All @@ -80,10 +78,10 @@ func TestCompositeErrorHandlerExecutionWithNoApplicableErrorHandler(t *testing.T
ctx.EXPECT().AppContext().Return(context.Background())

eh1 := rulemocks.NewErrorHandlerMock(t)
eh1.EXPECT().CanExecute(ctx, testsupport.ErrTestPurpose).Return(false)
eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(errErrorHandlerNotApplicable)

eh2 := rulemocks.NewErrorHandlerMock(t)
eh2.EXPECT().CanExecute(ctx, testsupport.ErrTestPurpose).Return(false)
eh2.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(errErrorHandlerNotApplicable)

eh := compositeErrorHandler{eh1, eh2}

Expand Down
Loading
Loading