From 5eb290a7d9eb2a853e0818d0470a0c986953d90d Mon Sep 17 00:00:00 2001 From: Frank Jogeleit Date: Wed, 11 Dec 2024 23:24:35 +0100 Subject: [PATCH] refactor: splitting authorization rule expression into match and response fields (#256) Signed-off-by: Frank Jogeleit --- ...nvoy.kyverno.io_authorizationpolicies.yaml | 10 +- .../policies/demo-policy.example.com.yaml | 35 ++-- .../authorizationpolicy-envoy-v1alpha1.json | 13 +- apis/v1alpha1/types.go | 7 +- .../kyverno-authz-server/templates/crds.yaml | 10 +- .../{env_test.go => libs/envoy/lib_test.go} | 6 +- pkg/policy/compiler.go | 83 +++++++--- pkg/policy/compiler_test.go | 154 ++++++++++++++++++ .../compilation-failure/chainsaw-test.yaml | 2 +- .../compilation-failure/policy.yaml | 2 +- .../invalid-output-type/chainsaw-test.yaml | 2 +- .../invalid-output-type/policy.yaml | 2 +- .../compilation-failure/chainsaw-test.yaml | 15 ++ .../compilation-failure/policy.yaml | 14 ++ .../invalid-output-type/chainsaw-test.yaml | 13 ++ .../invalid-output-type/policy.yaml | 14 ++ .../match-conditions/valid/chainsaw-test.yaml | 9 + .../match-conditions/valid/policy.yaml | 18 ++ .../authorizations/valid/policy.yaml | 34 ++-- .../compilation-failure/policy.yaml | 34 ++-- .../invalid-output-type/policy.yaml | 34 ++-- .../match-conditions/valid/policy.yaml | 34 ++-- .../docs/reference/apis/policy.v1alpha1.md | 3 +- 23 files changed, 421 insertions(+), 127 deletions(-) rename pkg/authz/cel/{env_test.go => libs/envoy/lib_test.go} (84%) create mode 100644 pkg/policy/compiler_test.go create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/chainsaw-test.yaml create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/policy.yaml create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/chainsaw-test.yaml create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/policy.yaml create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/valid/chainsaw-test.yaml create mode 100644 tests/e2e/validation-webhook/authorizations/match-conditions/valid/policy.yaml diff --git a/.crds/envoy.kyverno.io_authorizationpolicies.yaml b/.crds/envoy.kyverno.io_authorizationpolicies.yaml index b7f242ca..85fad11c 100644 --- a/.crds/envoy.kyverno.io_authorizationpolicies.yaml +++ b/.crds/envoy.kyverno.io_authorizationpolicies.yaml @@ -46,9 +46,13 @@ spec: items: description: Authorization defines an authorization policy rule properties: - expression: + match: + description: Match represents the match condition which will + be evaluated by CEL. Must evaluate to bool. + type: string + response: description: |- - Expression represents the expression which will be evaluated by CEL. + Response represents the response expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: @@ -57,7 +61,7 @@ spec: CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse). type: string required: - - expression + - response type: object type: array x-kubernetes-list-type: atomic diff --git a/.manifests/policies/demo-policy.example.com.yaml b/.manifests/policies/demo-policy.example.com.yaml index 91608336..323bd814 100644 --- a/.manifests/policies/demo-policy.example.com.yaml +++ b/.manifests/policies/demo-policy.example.com.yaml @@ -13,26 +13,25 @@ spec: expression: '{"my-new-metadata": "my-new-value"}' authorizations: # if force_unauthenticated -> 401 - - expression: > - variables.force_unauthenticated - ? envoy - .Denied(401) - .WithBody("Authentication Failed") - .Response() - : null + - match: variables.force_unauthenticated + response: > + envoy + .Denied(401) + .WithBody("Authentication Failed") + .Response() # if force_authorized -> 200 - - expression: > - variables.force_authorized - ? envoy - .Allowed() - .WithHeader("x-validated-by", "my-security-checkpoint") - .WithoutHeader("x-force-authorized") - .WithResponseHeader("x-add-custom-response-header", "added") - .Response() - .WithMetadata(variables.metadata) - : null + - match: variables.force_authorized + response: > + envoy + .Allowed() + .WithHeader("x-validated-by", "my-security-checkpoint") + .WithoutHeader("x-force-authorized") + .WithResponseHeader("x-add-custom-response-header", "added") + .Response() + .WithMetadata(variables.metadata) # else -> 403 - - expression: > + - match: 'true' + response: > envoy .Denied(403) .WithBody("Unauthorized Request") diff --git a/.schemas/json/authorizationpolicy-envoy-v1alpha1.json b/.schemas/json/authorizationpolicy-envoy-v1alpha1.json index 728b7b19..9c4234c1 100644 --- a/.schemas/json/authorizationpolicy-envoy-v1alpha1.json +++ b/.schemas/json/authorizationpolicy-envoy-v1alpha1.json @@ -340,11 +340,18 @@ "null" ], "required": [ - "expression" + "response" ], "properties": { - "expression": { - "description": "Expression represents the expression which will be evaluated by CEL.\nref: https://github.com/google/cel-spec\nCEL expressions have access to CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest)\n\nCEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).", + "match": { + "description": "Match represents the match condition which will be evaluated by CEL. Must evaluate to bool.", + "type": [ + "string", + "null" + ] + }, + "response": { + "description": "Response represents the response expression which will be evaluated by CEL.\nref: https://github.com/google/cel-spec\nCEL expressions have access to CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest)\n\nCEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).", "type": "string" } }, diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index fe7303e5..cccf11b8 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -76,7 +76,10 @@ func (s *AuthorizationPolicySpec) GetFailurePolicy() admissionregistrationv1.Fai // Authorization defines an authorization policy rule type Authorization struct { - // Expression represents the expression which will be evaluated by CEL. + // Match represents the match condition which will be evaluated by CEL. Must evaluate to bool. + // +optional + Match string `json:"match,omitempty"` + // Response represents the response expression which will be evaluated by CEL. // ref: https://github.com/google/cel-spec // CEL expressions have access to CEL variables as well as some other useful variables: // @@ -84,7 +87,7 @@ type Authorization struct { // // CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse). // +required - Expression string `json:"expression"` + Response string `json:"response"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/charts/kyverno-authz-server/templates/crds.yaml b/charts/kyverno-authz-server/templates/crds.yaml index 6e8147cb..3ce45bd0 100644 --- a/charts/kyverno-authz-server/templates/crds.yaml +++ b/charts/kyverno-authz-server/templates/crds.yaml @@ -55,9 +55,13 @@ spec: items: description: Authorization defines an authorization policy rule properties: - expression: + match: + description: Match represents the match condition which will + be evaluated by CEL. Must evaluate to bool. + type: string + response: description: |- - Expression represents the expression which will be evaluated by CEL. + Response represents the response expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: @@ -66,7 +70,7 @@ spec: CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse). type: string required: - - expression + - response type: object type: array x-kubernetes-list-type: atomic diff --git a/pkg/authz/cel/env_test.go b/pkg/authz/cel/libs/envoy/lib_test.go similarity index 84% rename from pkg/authz/cel/env_test.go rename to pkg/authz/cel/libs/envoy/lib_test.go index 60e86a10..68792a07 100644 --- a/pkg/authz/cel/env_test.go +++ b/pkg/authz/cel/libs/envoy/lib_test.go @@ -1,11 +1,13 @@ -package cel +package envoy_test import ( "reflect" "testing" authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" + "github.com/kyverno/kyverno-envoy-plugin/pkg/authz/cel/libs/envoy" "github.com/stretchr/testify/assert" ) @@ -19,7 +21,7 @@ envoy .WithMetadata({"my-new-metadata": "my-new-value"}) .WithMessage("hello") ` - env, err := NewEnv() + env, err := cel.NewEnv(envoy.Lib()) assert.NoError(t, err) ast, issues := env.Compile(source) assert.Nil(t, issues) diff --git a/pkg/policy/compiler.go b/pkg/policy/compiler.go index 8d7c2331..fd603d08 100644 --- a/pkg/policy/compiler.go +++ b/pkg/policy/compiler.go @@ -32,6 +32,11 @@ func NewCompiler() Compiler { type compiler struct{} func (c *compiler) Compile(policy *v1alpha1.AuthorizationPolicy) (PolicyFunc, field.ErrorList) { + type authorizationPrograms struct { + Match cel.Program + Response cel.Program + } + var allErrs field.ErrorList base, err := engine.NewEnv() if err != nil { @@ -51,17 +56,17 @@ func (c *compiler) Compile(policy *v1alpha1.AuthorizationPolicy) (PolicyFunc, fi { path := path.Child("matchConditions") for i, matchCondition := range policy.Spec.MatchConditions { - path := path.Index(i) + path := path.Index(i).Child("expression") ast, issues := env.Compile(matchCondition.Expression) if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), matchCondition.Expression, err.Error())) + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) } if !ast.OutputType().IsExactType(types.BoolType) { - return nil, append(allErrs, field.Invalid(path.Child("expression"), matchCondition.Expression, "matchCondition output is expected to be of type bool")) + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, "matchCondition output is expected to be of type bool")) } prog, err := env.Program(ast) if err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), matchCondition.Expression, err.Error())) + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) } matchConditions = append(matchConditions, prog) } @@ -70,36 +75,59 @@ func (c *compiler) Compile(policy *v1alpha1.AuthorizationPolicy) (PolicyFunc, fi { path := path.Child("variables") for i, variable := range policy.Spec.Variables { - path := path.Index(i) + path := path.Index(i).Child("expression") ast, issues := env.Compile(variable.Expression) if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), variable.Expression, err.Error())) + return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error())) } provider.RegisterField(variable.Name, ast.OutputType()) prog, err := env.Program(ast) if err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), variable.Expression, err.Error())) + return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error())) } variables[variable.Name] = prog } } - var authorizations []cel.Program + var authorizations []authorizationPrograms { path := path.Child("authorizations") for i, rule := range policy.Spec.Authorizations { path := path.Index(i) - ast, issues := env.Compile(rule.Expression) - if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), rule.Expression, err.Error())) - } - if !ast.OutputType().IsExactType(envoy.CheckResponse) { - return nil, append(allErrs, field.Invalid(path.Child("expression"), rule.Expression, "rule output is expected to be of type envoy.service.auth.v3.CheckResponse")) + program := authorizationPrograms{} + + if rule.Match != "" { + path := path.Child("match") + ast, issues := env.Compile(rule.Match) + if err := issues.Err(); err != nil { + return nil, append(allErrs, field.Invalid(path, rule.Match, err.Error())) + } + if !ast.OutputType().IsExactType(types.BoolType) { + return nil, append(allErrs, field.Invalid(path, rule.Match, "rule match output is expected to be of type bool")) + } + prog, err := env.Program(ast) + if err != nil { + return nil, append(allErrs, field.Invalid(path, rule.Match, err.Error())) + } + program.Match = prog } - prog, err := env.Program(ast) - if err != nil { - return nil, append(allErrs, field.Invalid(path.Child("expression"), rule.Expression, err.Error())) + + { + path := path.Child("response") + ast, issues := env.Compile(rule.Response) + if err := issues.Err(); err != nil { + return nil, append(allErrs, field.Invalid(path, rule.Response, err.Error())) + } + if !ast.OutputType().IsExactType(envoy.CheckResponse) { + return nil, append(allErrs, field.Invalid(path, rule.Response, "rule response output is expected to be of type envoy.service.auth.v3.CheckResponse")) + } + prog, err := env.Program(ast) + if err != nil { + return nil, append(allErrs, field.Invalid(path, rule.Response, err.Error())) + } + program.Response = prog } - authorizations = append(authorizations, prog) + + authorizations = append(authorizations, program) } } eval := func(r *authv3.CheckRequest) (*authv3.CheckResponse, error) { @@ -139,8 +167,25 @@ func (c *compiler) Compile(policy *v1alpha1.AuthorizationPolicy) (PolicyFunc, fi }) } for _, rule := range authorizations { + if rule.Match != nil { + // evaluate rule match condition + out, _, err := rule.Match.Eval(data) + if err != nil { + return nil, err + } + // try to convert to a match result + matched, err := utils.ConvertToNative[bool](out) + if err != nil { + return nil, err + } + // if condition is false, continue + if !matched { + continue + } + } + // evaluate the rule - out, _, err := rule.Eval(data) + out, _, err := rule.Response.Eval(data) // check error if err != nil { return nil, err diff --git a/pkg/policy/compiler_test.go b/pkg/policy/compiler_test.go new file mode 100644 index 00000000..043d0e8f --- /dev/null +++ b/pkg/policy/compiler_test.go @@ -0,0 +1,154 @@ +package policy_test + +import ( + "testing" + + "github.com/kyverno/kyverno-envoy-plugin/apis/v1alpha1" + "github.com/kyverno/kyverno-envoy-plugin/pkg/policy" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/stretchr/testify/assert" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" +) + +var pol = &v1alpha1.AuthorizationPolicy{ + Spec: v1alpha1.AuthorizationPolicySpec{ + Variables: []admissionregistrationv1.Variable{ + { + Name: "force_authorized", + Expression: `object.attributes.request.http.headers[?"x-force-authorized"].orValue("") in ["enabled", "true"]`, + }, + { + Name: "force_unauthenticated", + Expression: `object.attributes.request.http.headers[?"x-force-unauthenticated"].orValue("") in ["enabled", "true"]`, + }, + { + Name: "metadata", + Expression: `{"my-new-metadata": "my-new-value"}`, + }, + }, + Authorizations: []v1alpha1.Authorization{ + { + Match: "variables.force_unauthenticated", + Response: `envoy.Denied(401).WithBody("Authentication Failed").Response()`, + }, + { + Match: "variables.force_authorized", + Response: `envoy.Allowed().WithHeader("x-validated-by", "my-security-checkpoint").WithoutHeader("x-force-authorized").WithResponseHeader("x-add-custom-response-header", "added").Response().WithMetadata(variables.metadata)`, + }, + { + Response: `envoy.Denied(403).WithBody("Unauthorized Request").Response()`, + }, + }, + }, +} + +func TestCompiler(t *testing.T) { + compiler := policy.NewCompiler() + + function, errList := compiler.Compile(pol) + assert.NoError(t, errList.ToAggregate()) + + type testCase struct { + request *authv3.CheckRequest + responseType any + expectedHeaders map[string]string + unexpectedHeaders []string + body string + } + + tests := []testCase{ + { + request: &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{ + Headers: map[string]string{ + "x-force-authorized": "true", + }, + }, + }, + }, + }, + responseType: &authv3.CheckResponse_OkResponse{}, + expectedHeaders: map[string]string{ + "x-validated-by": "my-security-checkpoint", + }, + unexpectedHeaders: []string{"x-force-authorized"}, + }, + { + request: &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{ + Headers: map[string]string{ + "x-force-unauthenticated": "enabled", + }, + }, + }, + }, + }, + responseType: &authv3.CheckResponse_DeniedResponse{}, + body: "Authentication Failed", + }, + { + request: &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{ + Headers: make(map[string]string), + }, + }, + }, + }, + responseType: &authv3.CheckResponse_DeniedResponse{}, + body: "Unauthorized Request", + }, + } + + for _, test := range tests { + resp, err := function(test.request) + assert.NoError(t, err) + assert.NotNil(t, resp) + + ok := assert.IsType(t, test.responseType, resp.HttpResponse) + if !ok { + return + } + + var headers []*corev3.HeaderValueOption + var body string + + switch r := resp.HttpResponse.(type) { + case *authv3.CheckResponse_OkResponse: + headers = r.OkResponse.Headers + case *authv3.CheckResponse_DeniedResponse: + headers = r.DeniedResponse.Headers + body = r.DeniedResponse.Body + } + + for k, v := range test.expectedHeaders { + for _, header := range headers { + if header.Header.Key == k { + assert.Equal(t, header.Header.Value, v) + break + } + + assert.Failf(t, "missing '%s' header", k) + } + } + + for _, h := range test.expectedHeaders { + for _, header := range headers { + if header.Header.Key == h { + assert.Failf(t, "unexpected '%s' header", h) + } + } + } + + if test.body != "" { + assert.Equal(t, test.body, body) + } + } +} diff --git a/tests/e2e/validation-webhook/authorizations/compilation-failure/chainsaw-test.yaml b/tests/e2e/validation-webhook/authorizations/compilation-failure/chainsaw-test.yaml index eda0757b..405af84c 100644 --- a/tests/e2e/validation-webhook/authorizations/compilation-failure/chainsaw-test.yaml +++ b/tests/e2e/validation-webhook/authorizations/compilation-failure/chainsaw-test.yaml @@ -10,6 +10,6 @@ spec: expect: - check: ($error): |- - admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "compilation-failure" is invalid: spec.authorizations[0].expression: Invalid value: "envoy.Allowed() + 1\n": ERROR: :1:17: found no matching overload for '_+_' applied to '(envoy.service.auth.v3.OkHttpResponse, int)' + admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "compilation-failure" is invalid: spec.authorizations[0].response: Invalid value: "envoy.Allowed() + 1\n": ERROR: :1:17: found no matching overload for '_+_' applied to '(envoy.service.auth.v3.OkHttpResponse, int)' | envoy.Allowed() + 1 | ................^ diff --git a/tests/e2e/validation-webhook/authorizations/compilation-failure/policy.yaml b/tests/e2e/validation-webhook/authorizations/compilation-failure/policy.yaml index ab7df7de..e056c550 100644 --- a/tests/e2e/validation-webhook/authorizations/compilation-failure/policy.yaml +++ b/tests/e2e/validation-webhook/authorizations/compilation-failure/policy.yaml @@ -5,5 +5,5 @@ metadata: name: compilation-failure spec: authorizations: - - expression: > + - response: > envoy.Allowed() + 1 diff --git a/tests/e2e/validation-webhook/authorizations/invalid-output-type/chainsaw-test.yaml b/tests/e2e/validation-webhook/authorizations/invalid-output-type/chainsaw-test.yaml index 6a616295..5f3501d4 100644 --- a/tests/e2e/validation-webhook/authorizations/invalid-output-type/chainsaw-test.yaml +++ b/tests/e2e/validation-webhook/authorizations/invalid-output-type/chainsaw-test.yaml @@ -10,4 +10,4 @@ spec: expect: - check: ($error): |- - admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "invalid-output-type" is invalid: spec.authorizations[0].expression: Invalid value: "'bye'\n": rule output is expected to be of type envoy.service.auth.v3.CheckResponse + admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "invalid-output-type" is invalid: spec.authorizations[0].response: Invalid value: "'bye'\n": rule response output is expected to be of type envoy.service.auth.v3.CheckResponse diff --git a/tests/e2e/validation-webhook/authorizations/invalid-output-type/policy.yaml b/tests/e2e/validation-webhook/authorizations/invalid-output-type/policy.yaml index 3974873c..2fd56b94 100644 --- a/tests/e2e/validation-webhook/authorizations/invalid-output-type/policy.yaml +++ b/tests/e2e/validation-webhook/authorizations/invalid-output-type/policy.yaml @@ -5,5 +5,5 @@ metadata: name: invalid-output-type spec: authorizations: - - expression: > + - response: > 'bye' diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/chainsaw-test.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/chainsaw-test.yaml new file mode 100644 index 00000000..952c6744 --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/chainsaw-test.yaml @@ -0,0 +1,15 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: compilation-failure +spec: + steps: + - try: + - create: + file: ./policy.yaml + expect: + - check: + ($error): |- + admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "compilation-failure" is invalid: spec.authorizations[0].match: Invalid value: "'flop' + 2\n": ERROR: :1:8: found no matching overload for '_+_' applied to '(string, int)' + | 'flop' + 2 + | .......^ diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/policy.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/policy.yaml new file mode 100644 index 00000000..47530515 --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/compilation-failure/policy.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../../../../../.schemas/json/authorizationpolicy-envoy-v1alpha1.json +apiVersion: envoy.kyverno.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + name: compilation-failure +spec: + authorizations: + - match: > + 'flop' + 2 + response: > + envoy + .Denied(403) + .WithBody("Unauthorized Request") + .Response() diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/chainsaw-test.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/chainsaw-test.yaml new file mode 100644 index 00000000..a6b90579 --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/chainsaw-test.yaml @@ -0,0 +1,13 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: invalid-output-type +spec: + steps: + - try: + - create: + file: ./policy.yaml + expect: + - check: + ($error): |- + admission webhook "kyverno-authz-server-validation.kyverno.svc" denied the request: AuthorizationPolicy.envoy.kyverno.io "invalid-output-type" is invalid: spec.authorizations[0].match: Invalid value: "'flop'\n": rule match output is expected to be of type bool diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/policy.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/policy.yaml new file mode 100644 index 00000000..c6e250fb --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/invalid-output-type/policy.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../../../../../.schemas/json/authorizationpolicy-envoy-v1alpha1.json +apiVersion: envoy.kyverno.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + name: invalid-output-type +spec: + authorizations: + - match: > + 'flop' + response: > + envoy + .Denied(403) + .WithBody("Unauthorized Request") + .Response() diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/valid/chainsaw-test.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/valid/chainsaw-test.yaml new file mode 100644 index 00000000..df1e970c --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/valid/chainsaw-test.yaml @@ -0,0 +1,9 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: valid +spec: + steps: + - try: + - create: + file: ./policy.yaml diff --git a/tests/e2e/validation-webhook/authorizations/match-conditions/valid/policy.yaml b/tests/e2e/validation-webhook/authorizations/match-conditions/valid/policy.yaml new file mode 100644 index 00000000..eb38ef69 --- /dev/null +++ b/tests/e2e/validation-webhook/authorizations/match-conditions/valid/policy.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=../../../../../.schemas/json/authorizationpolicy-envoy-v1alpha1.json +apiVersion: envoy.kyverno.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + name: valid +spec: + authorizations: + - match: > + false + response: > + envoy + .Denied(403) + .WithBody("Unauthorized Request") + .Response() + - response: > + envoy + .Allowed() + .Response() diff --git a/tests/e2e/validation-webhook/authorizations/valid/policy.yaml b/tests/e2e/validation-webhook/authorizations/valid/policy.yaml index ef2ee3d9..895ce3b7 100644 --- a/tests/e2e/validation-webhook/authorizations/valid/policy.yaml +++ b/tests/e2e/validation-webhook/authorizations/valid/policy.yaml @@ -13,26 +13,24 @@ spec: expression: '{"my-new-metadata": "my-new-value"}' authorizations: # if force_unauthenticated -> 401 - - expression: > - variables.force_unauthenticated - ? envoy - .Denied(401) - .WithBody("Authentication Failed") - .Response() - : null + - match: variables.force_unauthenticated + response: > + envoy + .Denied(401) + .WithBody("Authentication Failed") + .Response() # if force_authorized -> 200 - - expression: > - variables.force_authorized - ? envoy - .Allowed() - .WithHeader("x-validated-by", "my-security-checkpoint") - .WithoutHeader("x-force-authorized") - .WithResponseHeader("x-add-custom-response-header", "added") - .Response() - .WithMetadata(variables.metadata) - : null + - match: variables.force_authorized + response: > + envoy + .Allowed() + .WithHeader("x-validated-by", "my-security-checkpoint") + .WithoutHeader("x-force-authorized") + .WithResponseHeader("x-add-custom-response-header", "added") + .Response() + .WithMetadata(variables.metadata) # else -> 403 - - expression: > + - response: > envoy .Denied(403) .WithBody("Unauthorized Request") diff --git a/tests/e2e/validation-webhook/match-conditions/compilation-failure/policy.yaml b/tests/e2e/validation-webhook/match-conditions/compilation-failure/policy.yaml index be163a31..2013fafb 100644 --- a/tests/e2e/validation-webhook/match-conditions/compilation-failure/policy.yaml +++ b/tests/e2e/validation-webhook/match-conditions/compilation-failure/policy.yaml @@ -17,26 +17,24 @@ spec: expression: '{"my-new-metadata": "my-new-value"}' authorizations: # if force_unauthenticated -> 401 - - expression: > - variables.force_unauthenticated - ? envoy - .Denied(401) - .WithBody("Authentication Failed") - .Response() - : null + - match: variables.force_unauthenticated + response: > + envoy + .Denied(401) + .WithBody("Authentication Failed") + .Response() # if force_authorized -> 200 - - expression: > - variables.force_authorized - ? envoy - .Allowed() - .WithHeader("x-validated-by", "my-security-checkpoint") - .WithoutHeader("x-force-authorized") - .WithResponseHeader("x-add-custom-response-header", "added") - .Response() - .WithMetadata(variables.metadata) - : null + - match: variables.force_authorized + response: > + envoy + .Allowed() + .WithHeader("x-validated-by", "my-security-checkpoint") + .WithoutHeader("x-force-authorized") + .WithResponseHeader("x-add-custom-response-header", "added") + .Response() + .WithMetadata(variables.metadata) # else -> 403 - - expression: > + - response: > envoy .Denied(403) .WithBody("Unauthorized Request") diff --git a/tests/e2e/validation-webhook/match-conditions/invalid-output-type/policy.yaml b/tests/e2e/validation-webhook/match-conditions/invalid-output-type/policy.yaml index d7e209bb..11205b0b 100644 --- a/tests/e2e/validation-webhook/match-conditions/invalid-output-type/policy.yaml +++ b/tests/e2e/validation-webhook/match-conditions/invalid-output-type/policy.yaml @@ -17,26 +17,24 @@ spec: expression: '{"my-new-metadata": "my-new-value"}' authorizations: # if force_unauthenticated -> 401 - - expression: > - variables.force_unauthenticated - ? envoy - .Denied(401) - .WithBody("Authentication Failed") - .Response() - : null + - match: variables.force_unauthenticated + response: > + envoy + .Denied(401) + .WithBody("Authentication Failed") + .Response() # if force_authorized -> 200 - - expression: > - variables.force_authorized - ? envoy - .Allowed() - .WithHeader("x-validated-by", "my-security-checkpoint") - .WithoutHeader("x-force-authorized") - .WithResponseHeader("x-add-custom-response-header", "added") - .Response() - .WithMetadata(variables.metadata) - : null + - match: variables.force_authorized + response: > + envoy + .Allowed() + .WithHeader("x-validated-by", "my-security-checkpoint") + .WithoutHeader("x-force-authorized") + .WithResponseHeader("x-add-custom-response-header", "added") + .Response() + .WithMetadata(variables.metadata) # else -> 403 - - expression: > + - response: > envoy .Denied(403) .WithBody("Unauthorized Request") diff --git a/tests/e2e/validation-webhook/match-conditions/valid/policy.yaml b/tests/e2e/validation-webhook/match-conditions/valid/policy.yaml index c8e35a2f..dbbb95a3 100644 --- a/tests/e2e/validation-webhook/match-conditions/valid/policy.yaml +++ b/tests/e2e/validation-webhook/match-conditions/valid/policy.yaml @@ -17,26 +17,24 @@ spec: expression: '{"my-new-metadata": "my-new-value"}' authorizations: # if force_unauthenticated -> 401 - - expression: > - variables.force_unauthenticated - ? envoy - .Denied(401) - .WithBody("Authentication Failed") - .Response() - : null + - match: variables.force_unauthenticated + response: > + envoy + .Denied(401) + .WithBody("Authentication Failed") + .Response() # if force_authorized -> 200 - - expression: > - variables.force_authorized - ? envoy - .Allowed() - .WithHeader("x-validated-by", "my-security-checkpoint") - .WithoutHeader("x-force-authorized") - .WithResponseHeader("x-add-custom-response-header", "added") - .Response() - .WithMetadata(variables.metadata) - : null + - match: variables.force_authorized + response: > + envoy + .Allowed() + .WithHeader("x-validated-by", "my-security-checkpoint") + .WithoutHeader("x-force-authorized") + .WithResponseHeader("x-add-custom-response-header", "added") + .Response() + .WithMetadata(variables.metadata) # else -> 403 - - expression: > + - response: > envoy .Denied(403) .WithBody("Unauthorized Request") diff --git a/website/docs/reference/apis/policy.v1alpha1.md b/website/docs/reference/apis/policy.v1alpha1.md index 3ffab430..5ae855c9 100644 --- a/website/docs/reference/apis/policy.v1alpha1.md +++ b/website/docs/reference/apis/policy.v1alpha1.md @@ -34,7 +34,8 @@ auto_generated: true | Field | Type | Required | Inline | Description | |---|---|---|---|---| -| `expression` | `string` | :white_check_mark: | |

Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: - 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest) CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).

| +| `match` | `string` | | |

Match represents the match condition which will be evaluated by CEL. Must evaluate to bool.

| +| `response` | `string` | :white_check_mark: | |

Response represents the response expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: - 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest) CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).

| ## AuthorizationPolicySpec {#envoy-kyverno-io-v1alpha1-AuthorizationPolicySpec}