From 27066876c239e848e3a07b8774bf3f1b6a963954 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Thu, 21 Sep 2023 00:12:21 +0200 Subject: [PATCH 1/4] feat: OR operator for the pattern-matching (conditions and pattern-matching authorization) Adds support for logical disjunction (aka "OR" operator) in 'when' conditions and pattern-matching authorization rules. A new field `any: Array` was introduced to the AuthConfig API to express logical disjunction, along with `all: Array` to enforce the default AND operation, where `expression` can be any of the following: - a pattern composed of `selector`, `operator` and `value`; - a reference to a named pattern (`patternRef: String`); - a nested `any` field; - a nested `all` field. For backward compatibility, the AND operator is assumed for all lists of patterns not grouped under an explicit `any` field declaration. I.e., it is equivalent to grouping the patterns under a single `all` field declaration. Example 1) To allow anonymous access (no authentication required) for all request to path `/test/*` OR method `GET` (ocasionally both): ```yaml spec: authentication: anonymous-request: when: - any: - selector: context.request.http.path operator: matches value: ^/test/.* - selector: context.request.http.method operator: eq method: GET anonymous: {} ``` In a more complex condition, with nested logical operators, to express `host == 'foo.apis.io' AND ((path =~ '/test*' AND (method == 'POST' OR method == 'PUT')) OR method == 'GET')`: ```yaml spec: authentication: anonymous-request: when: - selector: context.request.http.host operator: eq value: foo.apis.io - any: - all: - selector: context.request.http.path operator: matches value: ^/test/.* - any: - selector: context.request.http.method operator: eq value: POST - selector: context.request.http.method operator: eq value: PUT - selector: context.request.http.method operator: eq value: GET anonymous: {} ``` --- api/v1beta1/auth_config_types.go | 10 + api/v1beta1/zz_generated.deepcopy.go | 58 +- api/v1beta2/auth_config_conversion.go | 30 +- api/v1beta2/auth_config_types.go | 10 + api/v1beta2/zz_generated.deepcopy.go | 42 +- controllers/auth_config_controller.go | 72 ++- .../authorino.kuadrant.io_authconfigs.yaml | 210 +++++++ install/crd/patches/oneof_in_authconfigs.yaml | 72 +++ install/manifests.yaml | 306 ++++++++++ pkg/auth/auth.go | 4 +- pkg/auth/mocks/mock_auth.go | 6 +- pkg/evaluators/authorization.go | 12 +- pkg/evaluators/authorization/json.go | 24 +- pkg/evaluators/authorization/json_test.go | 196 +++---- pkg/evaluators/callbacks.go | 14 +- pkg/evaluators/config.go | 3 +- pkg/evaluators/identity.go | 12 +- pkg/evaluators/metadata.go | 12 +- pkg/evaluators/response.go | 17 +- pkg/json/json.go | 55 -- pkg/jsonexp/expressions.go | 178 ++++++ pkg/jsonexp/expressions_test.go | 551 ++++++++++++++++++ pkg/service/auth_pipeline.go | 17 +- pkg/service/auth_pipeline_test.go | 39 +- 24 files changed, 1673 insertions(+), 277 deletions(-) create mode 100644 pkg/jsonexp/expressions.go create mode 100644 pkg/jsonexp/expressions_test.go diff --git a/api/v1beta1/auth_config_types.go b/api/v1beta1/auth_config_types.go index f0535d99..d49a059a 100644 --- a/api/v1beta1/auth_config_types.go +++ b/api/v1beta1/auth_config_types.go @@ -150,6 +150,16 @@ type AuthConfigSpec struct { type JSONPattern struct { JSONPatternRef `json:",omitempty"` JSONPatternExpression `json:",omitempty"` + + // A list of pattern expressions to be evaluated as a logical AND. + All []UnstructuredJSONPattern `json:"all,omitempty"` + // A list of pattern expressions to be evaluated as a logical OR. + Any []UnstructuredJSONPattern `json:"any,omitempty"` +} + +type UnstructuredJSONPattern struct { + // +kubebuilder:pruning:PreserveUnknownFields + JSONPattern `json:",omitempty"` } type JSONPatternRef struct { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a7220b87..c70bb9df 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -133,7 +133,9 @@ func (in *AuthConfigSpec) DeepCopyInto(out *AuthConfigSpec) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Identity != nil { in, out := &in.Identity, &out.Identity @@ -236,7 +238,9 @@ func (in *Authorization) DeepCopyInto(out *Authorization) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Cache != nil { in, out := &in.Cache, &out.Cache @@ -312,7 +316,9 @@ func (in *Authorization_JSONPatternMatching) DeepCopyInto(out *Authorization_JSO if in.Rules != nil { in, out := &in.Rules, &out.Rules *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -412,7 +418,9 @@ func (in *Callback) DeepCopyInto(out *Callback) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.HTTP != nil { in, out := &in.HTTP, &out.HTTP @@ -582,7 +590,9 @@ func (in *Identity) DeepCopyInto(out *Identity) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Cache != nil { in, out := &in.Cache, &out.Cache @@ -774,6 +784,20 @@ func (in *JSONPattern) DeepCopyInto(out *JSONPattern) { *out = *in out.JSONPatternRef = in.JSONPatternRef out.JSONPatternExpression = in.JSONPatternExpression + if in.All != nil { + in, out := &in.All, &out.All + *out = make([]UnstructuredJSONPattern, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Any != nil { + in, out := &in.Any, &out.Any + *out = make([]UnstructuredJSONPattern, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPattern. @@ -858,7 +882,9 @@ func (in *Metadata) DeepCopyInto(out *Metadata) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Cache != nil { in, out := &in.Cache, &out.Cache @@ -1016,7 +1042,9 @@ func (in *Response) DeepCopyInto(out *Response) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]JSONPattern, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Cache != nil { in, out := &in.Cache, &out.Cache @@ -1192,6 +1220,22 @@ func (in *Summary) DeepCopy() *Summary { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UnstructuredJSONPattern) DeepCopyInto(out *UnstructuredJSONPattern) { + *out = *in + in.JSONPattern.DeepCopyInto(&out.JSONPattern) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnstructuredJSONPattern. +func (in *UnstructuredJSONPattern) DeepCopy() *UnstructuredJSONPattern { + if in == nil { + return nil + } + out := new(UnstructuredJSONPattern) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValueFrom) DeepCopyInto(out *ValueFrom) { *out = *in diff --git a/api/v1beta2/auth_config_conversion.go b/api/v1beta2/auth_config_conversion.go index d0acb1de..9d8e7e6d 100644 --- a/api/v1beta2/auth_config_conversion.go +++ b/api/v1beta2/auth_config_conversion.go @@ -216,21 +216,47 @@ func convertPatternExpressionFrom(src v1beta1.JSONPatternExpression) PatternExpr } func convertPatternExpressionOrRefTo(src PatternExpressionOrRef) v1beta1.JSONPattern { - return v1beta1.JSONPattern{ + pattern := v1beta1.JSONPattern{ JSONPatternExpression: convertPatternExpressionTo(src.PatternExpression), JSONPatternRef: v1beta1.JSONPatternRef{ JSONPatternName: src.PatternRef.Name, }, } + if len(src.All) > 0 { + pattern.All = make([]v1beta1.UnstructuredJSONPattern, len(src.All)) + for i, p := range src.All { + pattern.All[i] = v1beta1.UnstructuredJSONPattern{JSONPattern: convertPatternExpressionOrRefTo(p.PatternExpressionOrRef)} + } + } + if len(src.Any) > 0 { + pattern.Any = make([]v1beta1.UnstructuredJSONPattern, len(src.Any)) + for i, p := range src.Any { + pattern.Any[i] = v1beta1.UnstructuredJSONPattern{JSONPattern: convertPatternExpressionOrRefTo(p.PatternExpressionOrRef)} + } + } + return pattern } func convertPatternExpressionOrRefFrom(src v1beta1.JSONPattern) PatternExpressionOrRef { - return PatternExpressionOrRef{ + pattern := PatternExpressionOrRef{ PatternExpression: convertPatternExpressionFrom(src.JSONPatternExpression), PatternRef: PatternRef{ Name: src.JSONPatternRef.JSONPatternName, }, } + if len(src.All) > 0 { + pattern.All = make([]UnstructuredPatternExpressionOrRef, len(src.All)) + for i, p := range src.All { + pattern.All[i] = UnstructuredPatternExpressionOrRef{convertPatternExpressionOrRefFrom(p.JSONPattern)} + } + } + if len(src.Any) > 0 { + pattern.Any = make([]UnstructuredPatternExpressionOrRef, len(src.Any)) + for i, p := range src.Any { + pattern.Any[i] = UnstructuredPatternExpressionOrRef{convertPatternExpressionOrRefFrom(p.JSONPattern)} + } + } + return pattern } func convertEvaluatorCachingTo(src *EvaluatorCaching) *v1beta1.EvaluatorCaching { diff --git a/api/v1beta2/auth_config_types.go b/api/v1beta2/auth_config_types.go index dbec9df3..c689e51b 100644 --- a/api/v1beta2/auth_config_types.go +++ b/api/v1beta2/auth_config_types.go @@ -169,6 +169,16 @@ type PatternExpressionOperator string type PatternExpressionOrRef struct { PatternExpression `json:",omitempty"` PatternRef `json:",omitempty"` + + // A list of pattern expressions to be evaluated as a logical AND. + All []UnstructuredPatternExpressionOrRef `json:"all,omitempty"` + // A list of pattern expressions to be evaluated as a logical OR. + Any []UnstructuredPatternExpressionOrRef `json:"any,omitempty"` +} + +type UnstructuredPatternExpressionOrRef struct { + // +kubebuilder:pruning:PreserveUnknownFields + PatternExpressionOrRef `json:",omitempty"` } type PatternRef struct { diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 90aa19d4..3647917e 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -147,7 +147,9 @@ func (in *AuthConfigSpec) DeepCopyInto(out *AuthConfigSpec) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]PatternExpressionOrRef, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Authentication != nil { in, out := &in.Authentication, &out.Authentication @@ -485,7 +487,9 @@ func (in *CommonEvaluatorSpec) DeepCopyInto(out *CommonEvaluatorSpec) { if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]PatternExpressionOrRef, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Cache != nil { in, out := &in.Cache, &out.Cache @@ -994,6 +998,20 @@ func (in *PatternExpressionOrRef) DeepCopyInto(out *PatternExpressionOrRef) { *out = *in out.PatternExpression = in.PatternExpression out.PatternRef = in.PatternRef + if in.All != nil { + in, out := &in.All, &out.All + *out = make([]UnstructuredPatternExpressionOrRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Any != nil { + in, out := &in.Any, &out.Any + *out = make([]UnstructuredPatternExpressionOrRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatternExpressionOrRef. @@ -1031,7 +1049,9 @@ func (in *PatternMatchingAuthorizationSpec) DeepCopyInto(out *PatternMatchingAut if in.Patterns != nil { in, out := &in.Patterns, &out.Patterns *out = make([]PatternExpressionOrRef, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -1232,6 +1252,22 @@ func (in *UmaMetadataSpec) DeepCopy() *UmaMetadataSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UnstructuredPatternExpressionOrRef) DeepCopyInto(out *UnstructuredPatternExpressionOrRef) { + *out = *in + in.PatternExpressionOrRef.DeepCopyInto(&out.PatternExpressionOrRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnstructuredPatternExpressionOrRef. +func (in *UnstructuredPatternExpressionOrRef) DeepCopy() *UnstructuredPatternExpressionOrRef { + if in == nil { + return nil + } + out := new(UnstructuredPatternExpressionOrRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserInfoMetadataSpec) DeepCopyInto(out *UserInfoMetadataSpec) { *out = *in diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index 41610aa8..ba420b12 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -31,6 +31,7 @@ import ( response_evaluators "github.com/kuadrant/authorino/pkg/evaluators/response" "github.com/kuadrant/authorino/pkg/index" "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" "github.com/kuadrant/authorino/pkg/oauth2" "github.com/kuadrant/authorino/pkg/utils" @@ -181,7 +182,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf translatedIdentity := &evaluators.IdentityConfig{ Name: identity.Name, Priority: identity.Priority, - Conditions: buildJSONPatternExpressions(authConfig, identity.Conditions), + Conditions: buildJSONExpression(authConfig, identity.Conditions, jsonexp.All), ExtendedProperties: extendedProperties, Metrics: identity.Metrics, } @@ -276,7 +277,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf translatedMetadata := &evaluators.MetadataConfig{ Name: metadata.Name, Priority: metadata.Priority, - Conditions: buildJSONPatternExpressions(authConfig, metadata.Conditions), + Conditions: buildJSONExpression(authConfig, metadata.Conditions, jsonexp.All), Metrics: metadata.Metrics, } @@ -344,7 +345,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf translatedAuthorization := &evaluators.AuthorizationConfig{ Name: authorization.Name, Priority: authorization.Priority, - Conditions: buildJSONPatternExpressions(authConfig, authorization.Conditions), + Conditions: buildJSONExpression(authConfig, authorization.Conditions, jsonexp.All), Metrics: authorization.Metrics, } @@ -394,7 +395,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf // json case api.AuthorizationJSONPatternMatching: translatedAuthorization.JSON = &authorization_evaluators.JSONPatternMatching{ - Rules: buildJSONPatternExpressions(authConfig, authorization.JSON.Rules), + Rules: buildJSONExpression(authConfig, authorization.JSON.Rules, jsonexp.All), } case api.AuthorizationKubernetesAuthz: @@ -456,7 +457,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf translatedResponse := evaluators.NewResponseConfig( response.Name, response.Priority, - buildJSONPatternExpressions(authConfig, response.Conditions), + buildJSONExpression(authConfig, response.Conditions, jsonexp.All), string(response.Wrapper), response.WrapperKey, response.Metrics, @@ -560,7 +561,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf translatedCallback := &evaluators.CallbackConfig{ Name: callback.Name, Priority: callback.Priority, - Conditions: buildJSONPatternExpressions(authConfig, callback.Conditions), + Conditions: buildJSONExpression(authConfig, callback.Conditions, jsonexp.All), Metrics: callback.Metrics, } @@ -581,7 +582,7 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf } translatedAuthConfig := &evaluators.AuthConfig{ - Conditions: buildJSONPatternExpressions(authConfig, authConfig.Spec.Conditions), + Conditions: buildJSONExpression(authConfig, authConfig.Spec.Conditions, jsonexp.All), IdentityConfigs: interfacedIdentityConfigs, MetadataConfigs: interfacedMetadataConfigs, AuthorizationConfigs: interfacedAuthorizationConfigs, @@ -790,30 +791,55 @@ func findIdentityConfigByName(identityConfigs []evaluators.IdentityConfig, name return nil, fmt.Errorf("missing identity config %v", name) } -func buildJSONPatternExpressions(authConfig *api.AuthConfig, patterns []api.JSONPattern) []json.JSONPatternMatchingRule { - expressions := []json.JSONPatternMatchingRule{} - +func buildJSONExpression(authConfig *api.AuthConfig, patterns []api.JSONPattern, op func(...jsonexp.Expression) jsonexp.Expression) jsonexp.Expression { + var expression []jsonexp.Expression for _, pattern := range patterns { - expressionsToAdd := api.JSONPatternExpressions{} - - if expressionsByRef, found := authConfig.Spec.Patterns[pattern.JSONPatternName]; found { - expressionsToAdd = append(expressionsToAdd, expressionsByRef...) - } else { - expressionsToAdd = append(expressionsToAdd, pattern.JSONPatternExpression) + // patterns or refs + expression = append(expression, buildJSONExpressionPatterns(authConfig, pattern)...) + // all + if len(pattern.All) > 0 { + p := make([]api.JSONPattern, len(pattern.All)) + for i, ptn := range pattern.All { + p[i] = ptn.JSONPattern + } + expression = append(expression, buildJSONExpression(authConfig, p, jsonexp.All)) } - - for _, expression := range expressionsToAdd { - expressions = append(expressions, json.JSONPatternMatchingRule{ - Selector: expression.Selector, - Operator: string(expression.Operator), - Value: expression.Value, - }) + // any + if len(pattern.Any) > 0 { + p := make([]api.JSONPattern, len(pattern.Any)) + for i, ptn := range pattern.Any { + p[i] = ptn.JSONPattern + } + expression = append(expression, buildJSONExpression(authConfig, p, jsonexp.Any)) } } + return op(expression...) +} + +func buildJSONExpressionPatterns(authConfig *api.AuthConfig, pattern api.JSONPattern) []jsonexp.Expression { + expressionsToAdd := api.JSONPatternExpressions{} + if expressionsByRef, found := authConfig.Spec.Patterns[pattern.JSONPatternName]; found { + expressionsToAdd = append(expressionsToAdd, expressionsByRef...) + } else if pattern.JSONPatternExpression.Operator != "" { + expressionsToAdd = append(expressionsToAdd, pattern.JSONPatternExpression) + } + + expressions := make([]jsonexp.Expression, len(expressionsToAdd)) + for i, expression := range expressionsToAdd { + expressions[i] = buildJSONExpressionPattern(expression) + } return expressions } +func buildJSONExpressionPattern(expression api.JSONPatternExpression) jsonexp.Expression { + return jsonexp.Pattern{ + Selector: expression.Selector, + Operator: jsonexp.OperatorFromString(string(expression.Operator)), + Value: expression.Value, + } +} + func buildAuthorinoDenyWithValues(denyWithSpec *api.DenyWithSpec) *evaluators.DenyWithValues { if denyWithSpec == nil { return nil diff --git a/install/crd/authorino.kuadrant.io_authconfigs.yaml b/install/crd/authorino.kuadrant.io_authconfigs.yaml index 90a89374..800f877a 100644 --- a/install/crd/authorino.kuadrant.io_authconfigs.yaml +++ b/install/crd/authorino.kuadrant.io_authconfigs.yaml @@ -296,6 +296,20 @@ spec: for the request to be authorized. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -643,6 +657,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -926,6 +954,20 @@ spec: be attempted; otherwise, the callback will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -1473,6 +1515,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -1829,6 +1885,20 @@ spec: applied; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -2024,6 +2094,20 @@ spec: config to be enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -2156,6 +2240,20 @@ spec: OK. items: properties: + all: + description: A list of pattern expressions to be evaluated as + a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated as + a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison with "value". @@ -2606,6 +2704,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -3128,6 +3240,20 @@ spec: patterns: items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -3300,6 +3426,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -3591,6 +3731,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -3926,6 +4080,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -4114,6 +4282,20 @@ spec: will be skipped. items: properties: + all: + description: A list of pattern expressions to + be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to + be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization @@ -4330,6 +4512,20 @@ spec: will be skipped. items: properties: + all: + description: A list of pattern expressions to + be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to + be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization @@ -4585,6 +4781,20 @@ spec: request with status OK. items: properties: + all: + description: A list of pattern expressions to be evaluated as + a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated as + a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison with "value". diff --git a/install/crd/patches/oneof_in_authconfigs.yaml b/install/crd/patches/oneof_in_authconfigs.yaml index f805659c..8f61a5c8 100644 --- a/install/crd/patches/oneof_in_authconfigs.yaml +++ b/install/crd/patches/oneof_in_authconfigs.yaml @@ -103,6 +103,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/when/items/oneOf @@ -127,6 +133,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/metadata/items/properties/when/items/oneOf @@ -139,6 +151,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/authorization/items/properties/when/items/oneOf @@ -151,6 +169,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/response/items/properties/when/items/oneOf @@ -163,6 +187,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] # v1beta2 - op: add @@ -263,6 +293,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/when/items/oneOf @@ -275,6 +311,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/authentication/additionalProperties/properties/when/items/oneOf @@ -287,6 +329,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/metadata/additionalProperties/properties/when/items/oneOf @@ -299,6 +347,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/authorization/additionalProperties/properties/when/items/oneOf @@ -311,6 +365,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/response/properties/success/properties/headers/additionalProperties/properties/when/items/oneOf @@ -323,6 +383,12 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/response/properties/success/properties/dynamicMetadata/additionalProperties/properties/when/items/oneOf @@ -335,3 +401,9 @@ selector: {} value: {} required: [operator, selector] + - properties: + all: {} + required: [all] + - properties: + any: {} + required: [any] diff --git a/install/manifests.yaml b/install/manifests.yaml index ae05d895..01776178 100644 --- a/install/manifests.yaml +++ b/install/manifests.yaml @@ -342,7 +342,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -701,7 +723,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -985,6 +1029,20 @@ spec: be attempted; otherwise, the callback will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -1593,7 +1651,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -1980,7 +2060,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -2206,7 +2308,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -2351,6 +2475,20 @@ spec: - operator - selector properties: + all: + description: A list of pattern expressions to be evaluated as + a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated as + a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison with "value". @@ -2848,7 +2986,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -3399,7 +3559,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -3583,7 +3765,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -3875,6 +4079,20 @@ spec: enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -4234,7 +4452,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison @@ -4447,7 +4687,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to + be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to + be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization @@ -4688,7 +4950,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to + be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to + be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization @@ -4955,7 +5239,29 @@ spec: required: - operator - selector + - properties: + all: {} + required: + - all + - properties: + any: {} + required: + - any properties: + all: + description: A list of pattern expressions to be evaluated as + a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated as + a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, for comparison with "value". diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 6ee050e0..9b17e3b8 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,7 +3,7 @@ package auth import ( "golang.org/x/net/context" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" @@ -45,7 +45,7 @@ type Prioritizable interface { } type ConditionalEvaluator interface { - GetConditions() []json.JSONPatternMatchingRule + GetConditions() jsonexp.Expression } type IdentityConfigEvaluator interface { diff --git a/pkg/auth/mocks/mock_auth.go b/pkg/auth/mocks/mock_auth.go index 1a923801..8c9f3c03 100644 --- a/pkg/auth/mocks/mock_auth.go +++ b/pkg/auth/mocks/mock_auth.go @@ -10,7 +10,7 @@ import ( authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" gomock "github.com/golang/mock/gomock" auth "github.com/kuadrant/authorino/pkg/auth" - json "github.com/kuadrant/authorino/pkg/json" + jsonexp "github.com/kuadrant/authorino/pkg/jsonexp" context "golang.org/x/net/context" v1 "k8s.io/api/core/v1" labels "k8s.io/apimachinery/pkg/labels" @@ -335,10 +335,10 @@ func (m *MockConditionalEvaluator) EXPECT() *MockConditionalEvaluatorMockRecorde } // GetConditions mocks base method. -func (m *MockConditionalEvaluator) GetConditions() []json.JSONPatternMatchingRule { +func (m *MockConditionalEvaluator) GetConditions() jsonexp.Expression { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetConditions") - ret0, _ := ret[0].([]json.JSONPatternMatchingRule) + ret0, _ := ret[0].(jsonexp.Expression) return ret0 } diff --git a/pkg/evaluators/authorization.go b/pkg/evaluators/authorization.go index a9d7c082..75df06d3 100644 --- a/pkg/evaluators/authorization.go +++ b/pkg/evaluators/authorization.go @@ -6,7 +6,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/evaluators/authorization" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" ) @@ -18,10 +18,10 @@ const ( ) type AuthorizationConfig struct { - Name string `yaml:"name"` - Priority int `yaml:"priority"` - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` - Metrics bool `yaml:"metrics"` + Name string `yaml:"name"` + Priority int `yaml:"priority"` + Conditions jsonexp.Expression `yaml:"conditions"` + Metrics bool `yaml:"metrics"` Cache EvaluatorCache OPA *authorization.OPA `yaml:"opa,omitempty"` @@ -108,7 +108,7 @@ func (config *AuthorizationConfig) GetPriority() int { // impl:ConditionalEvaluator -func (config *AuthorizationConfig) GetConditions() []json.JSONPatternMatchingRule { +func (config *AuthorizationConfig) GetConditions() jsonexp.Expression { return config.Conditions } diff --git a/pkg/evaluators/authorization/json.go b/pkg/evaluators/authorization/json.go index 8cac542e..5d886fb6 100644 --- a/pkg/evaluators/authorization/json.go +++ b/pkg/evaluators/authorization/json.go @@ -5,23 +5,23 @@ import ( "fmt" "github.com/kuadrant/authorino/pkg/auth" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" ) type JSONPatternMatching struct { - Rules []json.JSONPatternMatchingRule + Rules jsonexp.Expression } -func (jsonAuth *JSONPatternMatching) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { - authJSON := pipeline.GetAuthorizationJSON() - - for _, rule := range jsonAuth.Rules { - if authorized, err := rule.EvaluateFor(authJSON); err != nil { - return false, err - } else if !authorized { - return false, fmt.Errorf(unauthorizedErrorMsg) - } +func (j *JSONPatternMatching) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { + if j.Rules == nil { + return true, nil + } + authorized, err := j.Rules.Matches(pipeline.GetAuthorizationJSON()) + if err != nil { + return false, err + } + if !authorized { + return false, fmt.Errorf(unauthorizedErrorMsg) } - return true, nil } diff --git a/pkg/evaluators/authorization/json_test.go b/pkg/evaluators/authorization/json_test.go index c7804d42..79cc266c 100644 --- a/pkg/evaluators/authorization/json_test.go +++ b/pkg/evaluators/authorization/json_test.go @@ -5,7 +5,7 @@ import ( "testing" mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" . "github.com/golang/mock/gomock" @@ -51,13 +51,11 @@ func TestCall(t *testing.T) { // eq with same value than expected jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "eq", - Value: "no-one-knows", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.EqualOperator, + Value: "no-one-knows", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -66,13 +64,11 @@ func TestCall(t *testing.T) { // eq with different value than expected jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "eq", - Value: "other-expected", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.EqualOperator, + Value: "other-expected", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -81,13 +77,11 @@ func TestCall(t *testing.T) { // neq with same value than expected jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "neq", - Value: "other-expected", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.NotEqualOperator, + Value: "other-expected", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -96,13 +90,11 @@ func TestCall(t *testing.T) { // neq with different value than expected jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "neq", - Value: "no-one-knows", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.NotEqualOperator, + Value: "no-one-knows", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -111,13 +103,11 @@ func TestCall(t *testing.T) { // incl with value found jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "auth.metadata.letters", - Operator: "incl", - Value: "a", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "auth.metadata.letters", + Operator: jsonexp.IncludesOperator, + Value: "a", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -126,13 +116,11 @@ func TestCall(t *testing.T) { // incl with value not found jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "auth.metadata.letters", - Operator: "incl", - Value: "d", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "auth.metadata.letters", + Operator: jsonexp.IncludesOperator, + Value: "d", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -141,13 +129,11 @@ func TestCall(t *testing.T) { // excl with value not found jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "auth.metadata.letters", - Operator: "excl", - Value: "d", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "auth.metadata.letters", + Operator: jsonexp.ExcludesOperator, + Value: "d", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -156,13 +142,11 @@ func TestCall(t *testing.T) { // excl with value found jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "auth.metadata.letters", - Operator: "excl", - Value: "b", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "auth.metadata.letters", + Operator: jsonexp.ExcludesOperator, + Value: "b", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -171,13 +155,11 @@ func TestCall(t *testing.T) { // regex matches value jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "matches", - Value: "(.+)-knows", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.RegexOperator, + Value: "(.+)-knows", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -186,13 +168,11 @@ func TestCall(t *testing.T) { // regex does not match value jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "matches", - Value: "(\\d)+", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.RegexOperator, + Value: "(\\d)+", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -201,13 +181,11 @@ func TestCall(t *testing.T) { // invalid regex jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { - Selector: "context.request.http.headers.x-secret-header", - Operator: "matches", - Value: "$$^[not-a-regex", - }, - }, + Rules: jsonexp.All(jsonexp.Pattern{ + Selector: "context.request.http.headers.x-secret-header", + Operator: jsonexp.RegexOperator, + Value: "$$^[not-a-regex", + }), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -216,33 +194,33 @@ func TestCall(t *testing.T) { // multiple rules jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { + Rules: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.headers.x-secret-header", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "no-one-knows", }, - { + jsonexp.Pattern{ Selector: "context.request.http.headers.x-secret-header", - Operator: "neq", + Operator: jsonexp.NotEqualOperator, Value: "other-expected", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "incl", + Operator: jsonexp.IncludesOperator, Value: "a", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "incl", + Operator: jsonexp.IncludesOperator, Value: "c", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "excl", + Operator: jsonexp.ExcludesOperator, Value: "d", }, - }, + ), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -251,33 +229,33 @@ func TestCall(t *testing.T) { // multiple rules with at least one unauthorized jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { + Rules: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.headers.x-secret-header", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "no-one-knows", }, - { + jsonexp.Pattern{ Selector: "context.request.http.headers.x-secret-header", - Operator: "neq", + Operator: jsonexp.NotEqualOperator, Value: "no-one-knows", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "incl", + Operator: jsonexp.IncludesOperator, Value: "xxxxx", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "incl", + Operator: jsonexp.IncludesOperator, Value: "c", }, - { + jsonexp.Pattern{ Selector: "auth.metadata.letters", - Operator: "excl", + Operator: jsonexp.ExcludesOperator, Value: "d", }, - }, + ), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -286,7 +264,7 @@ func TestCall(t *testing.T) { // rules empty jsonAuth = &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{}, + Rules: jsonexp.All(), } authorized, err = jsonAuth.Call(pipelineMock, nil) @@ -301,18 +279,18 @@ func BenchmarkJSONPatternMatchingAuthz(b *testing.B) { pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/allow"}}},"auth":{"identity":{"anonymous":true}}}`).MinTimes(1) jsonAuth := &JSONPatternMatching{ - Rules: []json.JSONPatternMatchingRule{ - { + Rules: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.method", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "GET", }, - { + jsonexp.Pattern{ Selector: "context.request.http.path", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "/allow", }, - }, + ), } var err error diff --git a/pkg/evaluators/callbacks.go b/pkg/evaluators/callbacks.go index 7253e7d2..7fbae6fb 100644 --- a/pkg/evaluators/callbacks.go +++ b/pkg/evaluators/callbacks.go @@ -6,13 +6,13 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/evaluators/metadata" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" ) const callbackHTTP = "CALLBACK_HTTP" -func NewCallbackConfig(name string, priority int, conditions []json.JSONPatternMatchingRule, metricsEnabled bool) *CallbackConfig { +func NewCallbackConfig(name string, priority int, conditions jsonexp.Expression, metricsEnabled bool) *CallbackConfig { callbackConfig := CallbackConfig{ Name: name, Priority: priority, @@ -24,10 +24,10 @@ func NewCallbackConfig(name string, priority int, conditions []json.JSONPatternM } type CallbackConfig struct { - Name string `yaml:"name"` - Priority int `yaml:"priority"` - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` - Metrics bool `yaml:"metrics"` + Name string `yaml:"name"` + Priority int `yaml:"priority"` + Conditions jsonexp.Expression `yaml:"conditions"` + Metrics bool `yaml:"metrics"` HTTP *metadata.GenericHttp `yaml:"http,omitempty"` } @@ -80,7 +80,7 @@ func (config *CallbackConfig) GetPriority() int { // impl:ConditionalEvaluator -func (config *CallbackConfig) GetConditions() []json.JSONPatternMatchingRule { +func (config *CallbackConfig) GetConditions() jsonexp.Expression { return config.Conditions } diff --git a/pkg/evaluators/config.go b/pkg/evaluators/config.go index fbd1f6cc..3d0f58c4 100644 --- a/pkg/evaluators/config.go +++ b/pkg/evaluators/config.go @@ -7,6 +7,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" multierror "github.com/hashicorp/go-multierror" ) @@ -14,7 +15,7 @@ import ( // AuthConfig holds the static configuration to be evaluated in the auth pipeline type AuthConfig struct { Labels map[string]string - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` + Conditions jsonexp.Expression `yaml:"conditions"` IdentityConfigs []auth.AuthConfigEvaluator `yaml:"identity,omitempty"` MetadataConfigs []auth.AuthConfigEvaluator `yaml:"metadata,omitempty"` diff --git a/pkg/evaluators/identity.go b/pkg/evaluators/identity.go index e16f6746..1c712f01 100644 --- a/pkg/evaluators/identity.go +++ b/pkg/evaluators/identity.go @@ -7,7 +7,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/evaluators/identity" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" v1 "k8s.io/api/core/v1" @@ -27,10 +27,10 @@ const ( ) type IdentityConfig struct { - Name string `yaml:"name"` - Priority int `yaml:"priority"` - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` - Metrics bool `yaml:"metrics"` + Name string `yaml:"name"` + Priority int `yaml:"priority"` + Conditions jsonexp.Expression `yaml:"conditions"` + Metrics bool `yaml:"metrics"` Cache EvaluatorCache OAuth2 *identity.OAuth2 `yaml:"oauth2,omitempty"` @@ -143,7 +143,7 @@ func (config *IdentityConfig) GetPriority() int { // impl:ConditionalEvaluator -func (config *IdentityConfig) GetConditions() []json.JSONPatternMatchingRule { +func (config *IdentityConfig) GetConditions() jsonexp.Expression { return config.Conditions } diff --git a/pkg/evaluators/metadata.go b/pkg/evaluators/metadata.go index 317b145d..e32eb01e 100644 --- a/pkg/evaluators/metadata.go +++ b/pkg/evaluators/metadata.go @@ -6,7 +6,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/evaluators/metadata" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" ) @@ -17,10 +17,10 @@ const ( ) type MetadataConfig struct { - Name string `yaml:"name"` - Priority int `yaml:"priority"` - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` - Metrics bool `yaml:"metrics"` + Name string `yaml:"name"` + Priority int `yaml:"priority"` + Conditions jsonexp.Expression `yaml:"conditions"` + Metrics bool `yaml:"metrics"` Cache EvaluatorCache UserInfo *metadata.UserInfo `yaml:"userinfo,omitempty"` @@ -102,7 +102,7 @@ func (config *MetadataConfig) GetPriority() int { // impl:ConditionalEvaluator -func (config *MetadataConfig) GetConditions() []json.JSONPatternMatchingRule { +func (config *MetadataConfig) GetConditions() jsonexp.Expression { return config.Conditions } diff --git a/pkg/evaluators/response.go b/pkg/evaluators/response.go index 9d9fcba5..6ee4b156 100644 --- a/pkg/evaluators/response.go +++ b/pkg/evaluators/response.go @@ -7,6 +7,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/evaluators/response" "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" ) @@ -21,7 +22,7 @@ const ( DEFAULT_WRAPPER = HTTP_HEADER_WRAPPER ) -func NewResponseConfig(name string, priority int, conditions []json.JSONPatternMatchingRule, wrapper string, wrapperKey string, metricsEnabled bool) *ResponseConfig { +func NewResponseConfig(name string, priority int, conditions jsonexp.Expression, wrapper string, wrapperKey string, metricsEnabled bool) *ResponseConfig { responseConfig := ResponseConfig{ Name: name, Priority: priority, @@ -43,12 +44,12 @@ func NewResponseConfig(name string, priority int, conditions []json.JSONPatternM } type ResponseConfig struct { - Name string `yaml:"name"` - Priority int `yaml:"priority"` - Conditions []json.JSONPatternMatchingRule `yaml:"conditions"` - Wrapper string `yaml:"wrapper"` - WrapperKey string `yaml:"wrapperKey"` - Metrics bool `yaml:"metrics"` + Name string `yaml:"name"` + Priority int `yaml:"priority"` + Conditions jsonexp.Expression `yaml:"conditions"` + Wrapper string `yaml:"wrapper"` + WrapperKey string `yaml:"wrapperKey"` + Metrics bool `yaml:"metrics"` Cache EvaluatorCache Wristband auth.WristbandIssuer `yaml:"wristband,omitempty"` @@ -130,7 +131,7 @@ func (config *ResponseConfig) GetPriority() int { // impl:ConditionalEvaluator -func (config *ResponseConfig) GetConditions() []json.JSONPatternMatchingRule { +func (config *ResponseConfig) GetConditions() jsonexp.Expression { return config.Conditions } diff --git a/pkg/json/json.go b/pkg/json/json.go index 4ffdd6b3..b7eddcd4 100644 --- a/pkg/json/json.go +++ b/pkg/json/json.go @@ -14,16 +14,6 @@ import ( "github.com/tidwall/gjson" ) -const ( - operatorEq = "eq" - operatorNeq = "neq" - operatorIncl = "incl" - operatorExcl = "excl" - operatorRegex = "matches" - - unsupportedOperatorErrorMsg = "unsupported operator for json authorization" -) - var ( allCurlyBracesRegex = regexp.MustCompile("{") curlyBracesForModifiersRegex = regexp.MustCompile(`[^@]+@\w+:{`) @@ -70,51 +60,6 @@ func (v *JSONValue) IsTemplate() bool { return len(curlyBracesForModifiersRegex.FindAllStringSubmatch(v.Pattern, -1)) != len(allCurlyBracesRegex.FindAllStringSubmatch(v.Pattern, -1)) } -type JSONPatternMatchingRule struct { - Selector string - Operator string - Value string -} - -func (rule *JSONPatternMatchingRule) EvaluateFor(jsonData string) (bool, error) { - expectedValue := rule.Value - obtainedValue := gjson.Get(jsonData, rule.Selector) - - switch rule.Operator { - case operatorEq: - return (expectedValue == obtainedValue.String()), nil - - case operatorNeq: - return (expectedValue != obtainedValue.String()), nil - - case operatorIncl: - for _, item := range obtainedValue.Array() { - if expectedValue == item.String() { - return true, nil - } - } - return false, nil - - case operatorExcl: - for _, item := range obtainedValue.Array() { - if expectedValue == item.String() { - return false, nil - } - } - return true, nil - - case operatorRegex: - if re, err := regexp.Compile(expectedValue); err != nil { - return false, err - } else { - return re.MatchString(obtainedValue.String()), nil - } - - default: - return false, fmt.Errorf(unsupportedOperatorErrorMsg) - } -} - // UnmashalJSONResponse unmarshalls a generic HTTP response body into a JSON structure // Pass optionally a pointer to a byte array to get the raw body of the response object written back func UnmashalJSONResponse(resp *http.Response, v interface{}, b *[]byte) error { diff --git a/pkg/jsonexp/expressions.go b/pkg/jsonexp/expressions.go new file mode 100644 index 00000000..6d56daef --- /dev/null +++ b/pkg/jsonexp/expressions.go @@ -0,0 +1,178 @@ +package jsonexp + +import ( + "fmt" + "regexp" + + "github.com/tidwall/gjson" +) + +type Operator int8 + +const ( + UnknownOperator Operator = iota + EqualOperator + NotEqualOperator + IncludesOperator + ExcludesOperator + RegexOperator +) + +func (o *Operator) String() string { + switch *o { + case EqualOperator: + return "eq" + case NotEqualOperator: + return "neq" + case IncludesOperator: + return "incl" + case ExcludesOperator: + return "excl" + case RegexOperator: + return "matches" + } + return "unknown" +} + +func OperatorFromString(operator string) Operator { + switch operator { + case "eq": + return EqualOperator + case "neq": + return NotEqualOperator + case "incl": + return IncludesOperator + case "excl": + return ExcludesOperator + case "matches": + return RegexOperator + } + return UnknownOperator +} + +type Pattern struct { + Selector string + Operator Operator + Value string +} + +func (p Pattern) Matches(json string) (bool, error) { + expectedValue := p.Value + obtainedValue := gjson.Get(json, p.Selector) + + switch p.Operator { + case EqualOperator: + return (expectedValue == obtainedValue.String()), nil + + case NotEqualOperator: + return (expectedValue != obtainedValue.String()), nil + + case IncludesOperator: + for _, item := range obtainedValue.Array() { + if expectedValue == item.String() { + return true, nil + } + } + return false, nil + + case ExcludesOperator: + for _, item := range obtainedValue.Array() { + if expectedValue == item.String() { + return false, nil + } + } + return true, nil + + case RegexOperator: + if re, err := regexp.Compile(expectedValue); err != nil { + return false, err + } else { + return re.MatchString(obtainedValue.String()), nil + } + + default: + return false, fmt.Errorf("unsupported operator for json authorization") + } +} + +func (p Pattern) String() string { + return fmt.Sprintf("%s %s %s", p.Selector, p.Operator.String(), p.Value) +} + +type Expression interface { + Matches(json string) (bool, error) +} + +type And struct { + Left Expression + Right Expression +} + +func (a *And) Matches(json string) (bool, error) { + if a.Left != nil { + left, err := a.Left.Matches(json) + if err != nil || !left { + return false, err + } + } + if a.Right != nil { + right, err := a.Right.Matches(json) + if err != nil || !right { + return false, err + } + } + return true, nil +} + +func (a *And) String() string { + return fmt.Sprintf("(%s && %s)", a.Left, a.Right) +} + +type Or struct { + Left Expression + Right Expression +} + +func (o *Or) Matches(json string) (bool, error) { + if o.Left != nil { + left, err := o.Left.Matches(json) + if err != nil { + return false, err + } + if left { + return true, nil + } + } + if o.Right != nil { + right, err := o.Right.Matches(json) + if err != nil { + return false, err + } + return right, nil + } + return false, nil +} + +func (o *Or) String() string { + return fmt.Sprintf("(%s || %s)", o.Left, o.Right) +} + +func All(expressions ...Expression) Expression { + if len(expressions) == 0 { + return &And{} + } + return &And{ + Left: expressions[0], + Right: All(expressions[1:]...), + } +} + +func Any(expressions ...Expression) Expression { + if len(expressions) == 0 { + return &Or{} + } + return &Or{ + Left: expressions[0], + Right: Any(expressions[1:]...), + } +} diff --git a/pkg/jsonexp/expressions_test.go b/pkg/jsonexp/expressions_test.go new file mode 100644 index 00000000..c10c4b8d --- /dev/null +++ b/pkg/jsonexp/expressions_test.go @@ -0,0 +1,551 @@ +package jsonexp + +import ( + "testing" + + "gotest.tools/assert" +) + +const testJsonData = `{ + "str": "my-value", + "int": 123, + "bool": true, + "obj": {"my-obj-str": "my-obj-value"}, + "arr": ["my-arr-value-1", "my-arr-value-2"] +}` + +func TestAnd(t *testing.T) { + // true && true + exp := &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // false && true + exp = &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + // true && false + exp = &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + // false && false + exp = &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} +func TestOneBranchAnd(t *testing.T) { + // true && nil + exp := &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // nil && true + exp = &And{ + Right: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // false && nil + exp = &And{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + // nil && false + exp = &And{ + Right: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestEmptyAnd(t *testing.T) { + // nil && nil + exp := &And{} + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) +} + +func TestOr(t *testing.T) { + // true || true + exp := &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // false || true + exp = &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // true || false + exp = &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // false || false + exp = &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Right: Pattern{ + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} +func TestOneBranchOr(t *testing.T) { + // true || nil + exp := &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // nil || true + exp = &Or{ + Right: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + // false || nil + exp = &Or{ + Left: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + // nil || false + exp = &Or{ + Right: Pattern{ + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestEmptyOr(t *testing.T) { + // nil || nil + exp := &Or{} + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestAll(t *testing.T) { + patterns := []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err := All(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + patterns = []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = All(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestTrivialAll(t *testing.T) { + patterns := []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err := All(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + patterns = []Expression{ + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = All(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestEmptyAll(t *testing.T) { + ok, err := All().Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) +} + +func TestAny(t *testing.T) { + patterns := []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + } + ok, err := Any(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + patterns = []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = Any(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) +} + +func TestTrivialAny(t *testing.T) { + patterns := []Expression{ + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + } + ok, err := Any(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + patterns = []Expression{ + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + } + ok, err = All(patterns...).Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestEmptyAny(t *testing.T) { + ok, err := Any().Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestAndOr(t *testing.T) { + exp := All( + Any( + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + ), + Any( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + ) + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) + + exp = All( + Any( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + ), + Any( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + ) + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) +} + +func TestOrAnd(t *testing.T) { + exp := Any( + All( + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + ), + All( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + ) + ok, err := exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + exp = Any( + All( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // false + Selector: "int", + Operator: EqualOperator, + Value: "wrong-value", + }, + ), + All( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + ) + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, !ok) + + exp = Any( + All( + Pattern{ // true + Selector: "str", + Operator: EqualOperator, + Value: "my-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + All( + Pattern{ // false + Selector: "str", + Operator: EqualOperator, + Value: "wrong-value", + }, + Pattern{ // true + Selector: "int", + Operator: EqualOperator, + Value: "123", + }, + ), + ) + ok, err = exp.Matches(testJsonData) + assert.NilError(t, err) + assert.Check(t, ok) +} diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index a4822bcf..dd50ea32 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -10,6 +10,7 @@ import ( "github.com/kuadrant/authorino/pkg/context" "github.com/kuadrant/authorino/pkg/evaluators" "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" "github.com/kuadrant/authorino/pkg/log" "github.com/kuadrant/authorino/pkg/metrics" @@ -374,14 +375,14 @@ func (pipeline *AuthPipeline) executeCallbacks() { } } -func (pipeline *AuthPipeline) evaluateConditions(conditions []json.JSONPatternMatchingRule) error { - authJSON := pipeline.GetAuthorizationJSON() - for _, condition := range conditions { - if match, err := condition.EvaluateFor(authJSON); err != nil { - return err - } else if !match { - return fmt.Errorf("unmatching conditions for config") - } +func (pipeline *AuthPipeline) evaluateConditions(conditions jsonexp.Expression) error { + if conditions == nil { + return nil + } + if match, err := conditions.Matches(pipeline.GetAuthorizationJSON()); err != nil { + return err + } else if !match { + return fmt.Errorf("unmatching conditions for config") } return nil } diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index 18cd3a0c..767fc701 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -13,6 +13,7 @@ import ( "github.com/kuadrant/authorino/pkg/evaluators/identity" "github.com/kuadrant/authorino/pkg/httptest" "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/jsonexp" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" @@ -48,7 +49,7 @@ var ( type successConfig struct { called bool priority int - conditions []json.JSONPatternMatchingRule + conditions jsonexp.Expression } type failConfig struct { @@ -65,7 +66,7 @@ func (c *successConfig) GetPriority() int { return c.priority } -func (c *successConfig) GetConditions() []json.JSONPatternMatchingRule { +func (c *successConfig) GetConditions() jsonexp.Expression { return c.conditions } @@ -393,13 +394,13 @@ func TestAuthPipelineWithUnmatchingConditionsInTheAuthConfig(t *testing.T) { idConfig := &successConfig{} pipeline := newTestAuthPipeline(evaluators.AuthConfig{ - Conditions: []json.JSONPatternMatchingRule{ - { + Conditions: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.path", - Operator: "neq", + Operator: jsonexp.NotEqualOperator, Value: "/operation", }, - }, + ), IdentityConfigs: []auth.AuthConfigEvaluator{idConfig}, }, &request) @@ -419,13 +420,13 @@ func TestAuthPipelineWithMatchingConditionsInTheAuthConfig(t *testing.T) { authzConfig := &successConfig{} pipeline := newTestAuthPipeline(evaluators.AuthConfig{ - Conditions: []json.JSONPatternMatchingRule{ - { + Conditions: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.path", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "/operation", }, - }, + ), IdentityConfigs: []auth.AuthConfigEvaluator{idConfig}, AuthorizationConfigs: []auth.AuthConfigEvaluator{authzConfig}, }, &request) @@ -444,13 +445,13 @@ func TestAuthPipelineWithUnmatchingConditionsInTheEvaluator(t *testing.T) { idConfig := &evaluators.IdentityConfig{Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it authzConfig := &successConfig{ - conditions: []json.JSONPatternMatchingRule{ - { + conditions: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.path", - Operator: "neq", + Operator: jsonexp.NotEqualOperator, Value: "/operation", }, - }, + ), } pipeline := newTestAuthPipeline(evaluators.AuthConfig{ @@ -472,13 +473,13 @@ func TestAuthPipelineWithMatchingConditionsInTheEvaluator(t *testing.T) { idConfig := &evaluators.IdentityConfig{Noop: &identity.Noop{}} // since it's going to be called and succeed, it has to be an actual config.IdentityConfig because AuthPipeline depends on it authzConfig := &successConfig{ - conditions: []json.JSONPatternMatchingRule{ - { + conditions: jsonexp.All( + jsonexp.Pattern{ Selector: "context.request.http.path", - Operator: "eq", + Operator: jsonexp.EqualOperator, Value: "/operation", }, - }, + ), } pipeline := newTestAuthPipeline(evaluators.AuthConfig{ @@ -564,7 +565,7 @@ func BenchmarkAuthPipeline(b *testing.B) { authCredMock.EXPECT().GetCredentialsKeySelector().Return("Bearer").AnyTimes() // this will only be invoked if the access token below is expired authCredMock.EXPECT().GetCredentialsFromReq(gomock.Any()).Return("eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ5cm0tSWpweGRfd3dzVmZPR1FUWWE2NHVmdEVlOHY3VG5sQzFMLUl4ZUlJIn0.eyJleHAiOjIxNDU4NjU3NzMsImlhdCI6MTY1OTA4ODE3MywianRpIjoiZDI0ODliMWEtYjY0Yi00MzRhLWJhNmItMmQ4OGIyY2I1ZWE3IiwiaXNzIjoiaHR0cDovL2tleWNsb2FrOjgwODAvYXV0aC9yZWFsbXMva3VhZHJhbnQiLCJhdWQiOlsicmVhbG0tbWFuYWdlbWVudCIsImFjY291bnQiXSwic3ViIjoiMWEwYjZjNmUtNDdmNy00ZjI1LWEyNjYtYzg3MzZhOTkxODQ0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZGVtbyIsInNlc3Npb25fc3RhdGUiOiIxMTdkMTc1Ni1mM2RlLTRjM2MtOWEwZS0zYjU5Mzc2YmI0ZTgiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwibWVtYmVyIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJyZWFsbS1tYW5hZ2VtZW50Ijp7InJvbGVzIjpbInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwidmlldy1yZWFsbSIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwicmVhbG0tYWRtaW4iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6IjExN2QxNzU2LWYzZGUtNGMzYy05YTBlLTNiNTkzNzZiYjRlOCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IlBldGVyIFdobyIsInByZWZlcnJlZF91c2VybmFtZSI6InBldGVyIiwiZ2l2ZW5fbmFtZSI6IlBldGVyIiwiZmFtaWx5X25hbWUiOiJXaG8iLCJlbWFpbCI6InBldGVyQGt1YWRyYW50LmlvIn0.Yy2aWR6_u0NBLx8x--OToYipfQ1f1KcC8zedsKDiymcbBiAaxrBQmaV2JC1PQVEgyxwmyMk0Rao2MdKGWk6pXB9mTUF5FX-pS8mkPIMUt1UVGJgzq7WR9KfRqdZSzRtFQHoDmTeA1-msayMYTAD8xtUH4JYRNbIXjY2cEtn8LjuLpQVR3DR4_ARMrEYXiDBS3rmmFKHdipqU7ozwJ_gtpZv8vfeiO3mUPyQLJKQ-nKpe_Z5z7tm_Ewh5MN2oBfn_0pcdANB3pe2RclGAm-YHlyNDTnAZL2Y1gdCmwzwigk7AJcgWtPqnRzvEQ9zRBxQRai5W5aNKYTxuKIG8k9N05w", nil).MinTimes(1) idConfig := &evaluators.IdentityConfig{OIDC: identity.NewOIDC(fmt.Sprintf("http://%v", oidcServerHost), authCredMock, 0, context.TODO())} - authzConfig := &evaluators.AuthorizationConfig{JSON: &authorization.JSONPatternMatching{Rules: []json.JSONPatternMatchingRule{{Selector: "auth.identity.realm_access.roles", Operator: "incl", Value: "member"}}}} + authzConfig := &evaluators.AuthorizationConfig{JSON: &authorization.JSONPatternMatching{Rules: jsonexp.All(jsonexp.Pattern{Selector: "auth.identity.realm_access.roles", Operator: jsonexp.IncludesOperator, Value: "member"})}} pipeline := newTestAuthPipeline(evaluators.AuthConfig{IdentityConfigs: []auth.AuthConfigEvaluator{idConfig}, AuthorizationConfigs: []auth.AuthConfigEvaluator{authzConfig}}, &request) var r auth.AuthResult From 70b2b1fa31b8b9cad5a590d6790684d3efa89845 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Thu, 21 Sep 2023 15:58:46 +0200 Subject: [PATCH 2/4] refactor: avoid else branch in the evaluation of jsonexp.RegexOperator --- pkg/jsonexp/expressions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/jsonexp/expressions.go b/pkg/jsonexp/expressions.go index 6d56daef..92981083 100644 --- a/pkg/jsonexp/expressions.go +++ b/pkg/jsonexp/expressions.go @@ -84,11 +84,11 @@ func (p Pattern) Matches(json string) (bool, error) { return true, nil case RegexOperator: - if re, err := regexp.Compile(expectedValue); err != nil { + re, err := regexp.Compile(expectedValue) + if err != nil { return false, err - } else { - return re.MatchString(obtainedValue.String()), nil } + return re.MatchString(obtainedValue.String()), nil default: return false, fmt.Errorf("unsupported operator for json authorization") From 302e420be557ae5ea87b58c69d8806ab6a2aad8c Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 22 Sep 2023 11:50:37 +0200 Subject: [PATCH 3/4] docs: AND and OR conditions --- docs/features.md | 202 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 153 insertions(+), 49 deletions(-) diff --git a/docs/features.md b/docs/features.md index 1505ab46..932200b5 100644 --- a/docs/features.md +++ b/docs/features.md @@ -874,30 +874,100 @@ For the `AuthConfig` above, ## Common feature: Conditions (`when`) -_Conditions_, named `when` in the AuthConfig API, are sets of expressions (JSON patterns) that, whenever included, must evaluate to true against the [Authorization JSON](./architecture.md#the-authorization-json), so the scope where the expressions are defined is enforced. If any of the expressions in the set of conditions for a given scope does not match, Authorino will skip that scope in the [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time). +_Conditions_, named `when` in the AuthConfig API, are logical expressions, composed of patterns and logical operator AND and OR, that can be used to condition the evaluation of a particular auth rule within an AuthConfig, as well as of the AuthConfig altogether ("top-level conditions"). -The scope for a set of `when` conditions can be the entire `AuthConfig` ("top-level conditions") or a particular evaluator of any phase of the auth pipeline. +The patterns are evaluated against the [Authorization JSON](./architecture.md#the-authorization-json), where each pattern is a tuple composed of: +- `selector`: a [JSON path](#common-feature-json-paths-selector) to fetch a value from the Authorization JSON +- `operator`: one of: `eq` (_equals_); `neq` (_not equal_); `incl` (_includes_) and `excl` (_excludes_), for when the value fetched from the Authorization JSON is expected to be an array; `matches`, for regular expressions +- `value`: a static string value to compare the value selected from the Authorization JSON with. -Each expression is a tuple composed of: -- a `selector`, to fetch from the Authorization JSON – see [Common feature: JSON paths](#common-feature-json-paths-selector) for details about syntax; -- an `operator` – `eq` (_equals_), `neq` (_not equal_); `incl` (_includes_) and `excl` (_excludes_), for arrays; and `matches`, for regular expressions; -- a fixed comparable `value` +An expression contains one or more patterns and they must either all evaluate to true ("AND" operator, declared by grouping the patterns within an `all` block) or at least one of the patterns must be true ("OR" operator, when grouped within an `any` block.) Patterns not explicitly grouped are AND'ed by default. + +To avoid repetitions when listing patterns, any set of literal `{ pattern, operator, value }` tuples can be stored at the top-level of the AuthConfig spec, indexed by name, and later referred within an expression by including a `patternRef` in the block of conditions. + +**Examples of `when` conditions** + +i) to skip an entire `AuthConfig` based on the context (AND operator assumed by default): + +```yaml +spec: + when: # auth enforced only on requests to POST /resources/* + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.path + operator: matches + value: ^/resources/.* +``` + +ii) equivalent to the above with explicit AND operator (i.e., `all` block): + +```yaml +spec: + when: # auth enforced only on requests to POST /resources/* + - all: + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.path + operator: matches + value: ^/resources/.* +``` + +iii) OR condition (i.e., `any` block): + +```yaml +spec: + when: # auth enforced only on requests with HTTP method equals to POST or PUT + - any: + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.method + operator: eq + value: PUT +``` -Literal expressions and references to expression sets (`patterns`, defined at the upper level of the `AuthConfig` spec) can be listed, mixed and combined in `when` conditions sets. +iv) complex expression with nested operations: -_Conditions_ can be used, e.g.,: +```yaml +spec: + when: # auth enforced only on requests to POST /resources/* or PUT /resources/* + - any: + - all: + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.path + operator: matches + value: ^/resources/.* + - all: + - selector: context.request.http.method + operator: eq + value: PUT + - selector: context.request.http.path + operator: matches + value: ^/resources/.* +``` -i) to skip an entire `AuthConfig` based on the context: +v) more concise equivalent of the above (with implicit AND operator at the top level): ```yaml spec: - when: # no authn/authz required on requests to /status + when: # auth enforced only on requests to /resources/* path with method equals to POST or PUT - selector: context.request.http.path - operator: neq - value: /status + operator: matches + value: ^/resources/.* + - any: + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.method + operator: eq + value: PUT ``` -ii) to skip parts of an `AuthConfig` (i.e. a specific evaluator): +vi) to skip part of an AuthConfig (i.e., a specific auth rule): ```yaml spec: @@ -911,26 +981,46 @@ spec: value: OPTIONS ``` -iii) to enforce a particular evaluator only in certain contexts (really the same as the above, though to a different use case): +vii) skipping part of an AuthConfig will not affect other auth rules: ```yaml spec: authentication: "authn-meth-1": - apiKey: {…} # this authn method only valid for POST requests to /foo[/*] + apiKey: {…} # this auth rule only triggers for POST requests to /foo[/*] when: - - selector: context.request.http.path - operator: matches - value: ^/foo(/.*)?$ - selector: context.request.http.method operator: eq value: POST + - selector: context.request.http.path + operator: matches + value: ^/foo(/.*)?$ - "authn-meth-2": + "authn-meth-2": # this auth rule triggerred regardless jwt: {…} ``` -iv) to avoid repetition while defining patterns for conditions: +viii) concrete use-case: evaluating only the necessary identity checks based on the user's indication of the preferred authentication method (prefix of the value supplied in the HTTP `Authorization` request header): + +```yaml +spec: + authentication: + "jwt": + when: + - selector: context.request.http.headers.authorization + operator: matches + value: JWT .+ + jwt: {…} + + "api-key": + when: + - selector: context.request.http.headers.authorization + operator: matches + value: APIKEY .+ + apiKey: {…} +``` + +ix) to avoid repetition while defining patterns for conditions: ```yaml spec: @@ -956,47 +1046,61 @@ spec: allow { input.metadata["pets-info"].ownerid == input.auth.identity.userid } ``` -v) mixing and combining literal expressions and refs: +x) combining literals and refs – concrete case: authentication required for selected operations: ```yaml spec: patterns: - foo: + api-base-path: - selector: context.request.http.path - operator: eq - value: /foo - - when: # unauthenticated access to /foo always granted - - patternRef: foo - - selector: context.request.http.headers.authorization - operator: eq - value: "" - - authorization: - "my-policy-1": - when: # authenticated access to /foo controlled by policy - - patternRef: foo - patternMatching: {…} -``` + operator: matches + value: ^/api/.* -vi) to avoid evaluating unnecessary identity checks when the user can indicate the preferred authentication method (again the pattern of skipping based upon the context): + authenticated-user: + - selector: auth.identity.anonymous + operator: neq + value: "true" -```yaml -spec: authentication: - "jwt": + api-users: # tries to authenticate all requests to path /api/* when: - - selector: context.request.http.headers.authorization - operator: matches - value: JWT .+ + - patternRef: api-base-path jwt: {…} - "api-key": + others: # defaults to anonymous access when authentication fails or not /api/* path + anonymous: {} + priority: 1 + + authorization: + api-write-access-requires-authentication: # POST/PUT/DELETE requests to /api/* path cannot be anonymous when: - - selector: context.request.http.headers.authorization - operator: matches - value: APIKEY .+ - apiKey: {…} + - all: + - patternRef: api-base-path + - any: + - selector: context.request.http.method + operator: eq + value: POST + - selector: context.request.http.method + operator: eq + value: PUT + - selector: context.request.http.method + operator: eq + value: DELETE + opa: + patternMathing: + rules: + - patternRef: authenticated-user + + response: # bonus: export user data if available + success: + dynamicMetadata: + "user-data": + when: + - patternRef: authenticated-user + json: + properties: + jwt-claims: + selector: auth.identity ``` ## Common feature: Caching (`cache`) From 3ec4f6b117b538123508bc354ddee405365797a4 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 26 Sep 2023 09:52:37 +0200 Subject: [PATCH 4/4] docs: fix typo at: AND and OR conditions --- docs/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.md b/docs/features.md index 932200b5..136d736d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1087,7 +1087,7 @@ spec: operator: eq value: DELETE opa: - patternMathing: + patternMatching: rules: - patternRef: authenticated-user