mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 09:52:49 +00:00
Merge pull request #116054 from jpbetz/secondary-authz
KEP-3488: Implement secondary authz for ValidatingAdmissionPolicy
This commit is contained in:
commit
64259b43b8
2
api/openapi-spec/swagger.json
generated
2
api/openapi-spec/swagger.json
generated
@ -637,7 +637,7 @@
|
||||
"description": "Validation specifies the CEL expression which is used to apply the validation.",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"description": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"description": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. The value is null for DELETE requests. - 'oldObject' - The existing object. The value is null for CREATE requests. - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
|
@ -375,7 +375,7 @@
|
||||
"properties": {
|
||||
"expression": {
|
||||
"default": "",
|
||||
"description": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"description": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. The value is null for DELETE requests. - 'oldObject' - The existing object. The value is null for CREATE requests. - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
|
@ -186,10 +186,14 @@ type Validation struct {
|
||||
// ref: https://github.com/google/cel-spec
|
||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
||||
//
|
||||
//'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
//'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
//'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
//'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||
// request resource.
|
||||
//
|
||||
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||
// object. No other metadata properties are accessible.
|
||||
|
@ -18,10 +18,11 @@ package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
||||
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/api/validation/path"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -32,6 +33,7 @@ import (
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
||||
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||
@ -738,7 +740,7 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
||||
Expression: trimmedExpression,
|
||||
Message: v.Message,
|
||||
Reason: v.Reason,
|
||||
}, paramKind != nil)
|
||||
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true})
|
||||
if result.Error != nil {
|
||||
switch result.Error.Type {
|
||||
case cel.ErrorTypeRequired:
|
||||
|
2
pkg/generated/openapi/zz_generated.openapi.go
generated
2
pkg/generated/openapi/zz_generated.openapi.go
generated
@ -2407,7 +2407,7 @@ func schema_k8sio_api_admissionregistration_v1alpha1_Validation(ref common.Refer
|
||||
Properties: map[string]spec.Schema{
|
||||
"expression": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
Description: "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. The value is null for DELETE requests. - 'oldObject' - The existing object. The value is null for CREATE requests. - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
|
@ -263,10 +263,14 @@ message Validation {
|
||||
// ref: https://github.com/google/cel-spec
|
||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
||||
//
|
||||
// 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
// 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
// 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
// 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||
// request resource.
|
||||
//
|
||||
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||
// object. No other metadata properties are accessible.
|
||||
|
@ -140,10 +140,14 @@ type Validation struct {
|
||||
// ref: https://github.com/google/cel-spec
|
||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
||||
//
|
||||
//'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
//'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
//'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
//'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||
// request resource.
|
||||
//
|
||||
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||
// object. No other metadata properties are accessible.
|
||||
|
@ -134,7 +134,7 @@ func (ValidatingAdmissionPolicySpec) SwaggerDoc() map[string]string {
|
||||
|
||||
var map_Validation = map[string]string{
|
||||
"": "Validation specifies the CEL expression which is used to apply the validation.",
|
||||
"expression": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"expression": "Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:\n\n- 'object' - The object from the incoming request. The value is null for DELETE requests. - 'oldObject' - The existing object. The value is null for CREATE requests. - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)). - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind. - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n- 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object. No other metadata properties are accessible.\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Expression accessing a property named \"namespace\": {\"Expression\": \"object.__namespace__ > 0\"}\n - Expression accessing a property named \"x-prop\": {\"Expression\": \"object.x__dash__prop > 0\"}\n - Expression accessing a property named \"redact__d\": {\"Expression\": \"object.redact__underscores__d > 0\"}\n\nEquality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.\nRequired.",
|
||||
"message": "Message represents the message displayed when validation fails. The message is required if the Expression contains line breaks. The message must not contain line breaks. If unset, the message is \"failed rule: {Rule}\". e.g. \"must be a URL with the host matching spec.host\" If the Expression contains line breaks. Message is required. The message must not contain line breaks. If unset, the message is \"failed Expression: {Expression}\".",
|
||||
"reason": "Reason represents a machine-readable description of why this validation failed. If this is the first validation in the list to fail, this reason, as well as the corresponding HTTP response code, are used in the HTTP response to the client. The currently supported reasons are: \"Unauthorized\", \"Forbidden\", \"Invalid\", \"RequestEntityTooLarge\". If not set, StatusReasonInvalid is used in the response to the client.",
|
||||
}
|
||||
|
@ -1751,6 +1751,14 @@ func TestValidationExpressions(t *testing.T) {
|
||||
"oldSelf == self",
|
||||
},
|
||||
},
|
||||
{name: "authorizer is not supported for CRD Validation Rules",
|
||||
obj: []interface{}{},
|
||||
oldObj: []interface{}{},
|
||||
schema: objectTypePtr(map[string]schema.Structural{}),
|
||||
errors: map[string]string{
|
||||
"authorizer.path('/healthz').check('get').isAllowed()": "undeclared reference to 'authorizer'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
@ -26,43 +27,35 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ObjectVarName = "object"
|
||||
OldObjectVarName = "oldObject"
|
||||
ParamsVarName = "params"
|
||||
RequestVarName = "request"
|
||||
ObjectVarName = "object"
|
||||
OldObjectVarName = "oldObject"
|
||||
ParamsVarName = "params"
|
||||
RequestVarName = "request"
|
||||
AuthorizerVarName = "authorizer"
|
||||
RequestResourceAuthorizerVarName = "authorizer.requestResource"
|
||||
|
||||
checkFrequency = 100
|
||||
)
|
||||
|
||||
type envs struct {
|
||||
noParams *cel.Env
|
||||
withParams *cel.Env
|
||||
}
|
||||
|
||||
var (
|
||||
initEnvsOnce sync.Once
|
||||
initEnvs *envs
|
||||
initEnvs envs
|
||||
initEnvsErr error
|
||||
)
|
||||
|
||||
func getEnvs() (*envs, error) {
|
||||
func getEnvs() (envs, error) {
|
||||
initEnvsOnce.Do(func() {
|
||||
base, err := buildBaseEnv()
|
||||
requiredVarsEnv, err := buildRequiredVarsEnv()
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
noParams, err := buildNoParamsEnv(base)
|
||||
|
||||
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
withParams, err := buildWithParamsEnv(noParams)
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
initEnvs = &envs{noParams: noParams, withParams: withParams}
|
||||
})
|
||||
return initEnvs, initEnvsErr
|
||||
}
|
||||
@ -81,7 +74,11 @@ func buildBaseEnv() (*cel.Env, error) {
|
||||
return cel.NewEnv(opts...)
|
||||
}
|
||||
|
||||
func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
|
||||
func buildRequiredVarsEnv() (*cel.Env, error) {
|
||||
baseEnv, err := buildBaseEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var propDecls []cel.EnvOption
|
||||
reg := apiservercel.NewRegistry(baseEnv)
|
||||
|
||||
@ -109,8 +106,33 @@ func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) {
|
||||
return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType))
|
||||
type envs map[OptionalVariableDeclarations]*cel.Env
|
||||
|
||||
func buildEnvWithVars(baseVarsEnv *cel.Env, options OptionalVariableDeclarations) (*cel.Env, error) {
|
||||
var opts []cel.EnvOption
|
||||
if options.HasParams {
|
||||
opts = append(opts, cel.Variable(ParamsVarName, cel.DynType))
|
||||
}
|
||||
if options.HasAuthorizer {
|
||||
opts = append(opts, cel.Variable(AuthorizerVarName, library.AuthorizerType))
|
||||
opts = append(opts, cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||
}
|
||||
return baseVarsEnv.Extend(opts...)
|
||||
}
|
||||
|
||||
func buildWithOptionalVarsEnvs(requiredVarsEnv *cel.Env) (envs, error) {
|
||||
envs := make(envs, 4) // since the number of variable combinations is small, pre-build a environment for each
|
||||
for _, hasParams := range []bool{false, true} {
|
||||
for _, hasAuthorizer := range []bool{false, true} {
|
||||
opts := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}
|
||||
env, err := buildEnvWithVars(requiredVarsEnv, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
envs[opts] = env
|
||||
}
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
|
||||
@ -168,7 +190,7 @@ type CompilationResult struct {
|
||||
}
|
||||
|
||||
// CompileCELExpression returns a compiled CEL expression.
|
||||
func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool) CompilationResult {
|
||||
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations) CompilationResult {
|
||||
var env *cel.Env
|
||||
envs, err := getEnvs()
|
||||
if err != nil {
|
||||
@ -180,10 +202,15 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool)
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
if hasParams {
|
||||
env = envs.withParams
|
||||
} else {
|
||||
env = envs.noParams
|
||||
env, ok := envs[optionalVars]
|
||||
if !ok {
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("compiler initialization failed: failed to load environment for %v", optionalVars),
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
|
||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||
|
@ -26,6 +26,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
name string
|
||||
expressions []string
|
||||
hasParams bool
|
||||
hasAuthorizer bool
|
||||
errorExpressions map[string]string
|
||||
}{
|
||||
{
|
||||
@ -99,6 +100,19 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
"request.userInfo.foo5 == 'nope'": "undefined field 'foo5'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with authorizer",
|
||||
hasAuthorizer: true,
|
||||
expressions: []string{
|
||||
"authorizer.group('') != null",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "without authorizer",
|
||||
errorExpressions: map[string]string{
|
||||
"authorizer.group('') != null": "undeclared reference to 'authorizer'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@ -106,7 +120,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
for _, expr := range tc.expressions {
|
||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||
expr,
|
||||
}, tc.hasParams)
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true})
|
||||
if result.Error != nil {
|
||||
t.Errorf("Unexpected error: %v", result.Error)
|
||||
}
|
||||
@ -114,7 +128,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
for expr, expectErr := range tc.errorExpressions {
|
||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||
expr,
|
||||
}, tc.hasParams)
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer})
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||
continue
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// filterCompiler implement the interface FilterCompiler.
|
||||
@ -42,7 +43,7 @@ func NewFilterCompiler() FilterCompiler {
|
||||
}
|
||||
|
||||
type evaluationActivation struct {
|
||||
object, oldObject, params, request interface{}
|
||||
object, oldObject, params, request, authorizer, requestResourceAuthorizer interface{}
|
||||
}
|
||||
|
||||
// ResolveName returns a value from the activation by qualified name, or false if the name
|
||||
@ -54,9 +55,13 @@ func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
|
||||
case OldObjectVarName:
|
||||
return a.oldObject, true
|
||||
case ParamsVarName:
|
||||
return a.params, true
|
||||
return a.params, true // params may be null
|
||||
case RequestVarName:
|
||||
return a.request, true
|
||||
case AuthorizerVarName:
|
||||
return a.authorizer, a.authorizer != nil
|
||||
case RequestResourceAuthorizerVarName:
|
||||
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
@ -69,13 +74,13 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
|
||||
}
|
||||
|
||||
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, hasParam bool) Filter {
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations) Filter {
|
||||
if len(expressionAccessors) == 0 {
|
||||
return nil
|
||||
}
|
||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||
for i, expressionAccessor := range expressionAccessors {
|
||||
compilationResults[i] = CompileCELExpression(expressionAccessor, hasParam)
|
||||
compilationResults[i] = CompileCELExpression(expressionAccessor, options)
|
||||
}
|
||||
return NewFilter(compilationResults)
|
||||
}
|
||||
@ -113,9 +118,9 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
||||
return v.Object, nil
|
||||
}
|
||||
|
||||
// Evaluate evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||
// errors per evaluation are returned on the Evaluation object
|
||||
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]EvaluationResult, error) {
|
||||
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings) ([]EvaluationResult, error) {
|
||||
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
||||
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
||||
var err error
|
||||
@ -128,9 +133,17 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paramsVal, err := objectToResolveVal(versionedParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||
if inputs.VersionedParams != nil {
|
||||
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if inputs.Authorizer != nil {
|
||||
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
|
||||
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
|
||||
}
|
||||
|
||||
requestVal, err := convertObjectToUnstructured(request)
|
||||
@ -138,10 +151,12 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
||||
return nil, err
|
||||
}
|
||||
va := &evaluationActivation{
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
authorizer: authorizerVal,
|
||||
requestResourceAuthorizer: requestResourceAuthorizerVal,
|
||||
}
|
||||
|
||||
for i, compilationResult := range f.compilationResults {
|
||||
|
@ -17,12 +17,18 @@ limitations under the License.
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@ -81,7 +87,7 @@ func TestCompile(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c filterCompiler
|
||||
e := c.Compile(tc.validation, false)
|
||||
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false})
|
||||
if e == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
@ -144,6 +150,7 @@ func TestFilter(t *testing.T) {
|
||||
validations []ExpressionAccessor
|
||||
results []EvaluationResult
|
||||
hasParamKind bool
|
||||
authorizer authorizer.Authorizer
|
||||
}{
|
||||
{
|
||||
name: "valid syntax for object",
|
||||
@ -417,12 +424,204 @@ func TestFilter(t *testing.T) {
|
||||
hasParamKind: true,
|
||||
params: runtime.Object(nilUnstructured),
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
Resource: "endpoints",
|
||||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check with all fields",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: "apps",
|
||||
Resource: "deployments",
|
||||
Subresource: "status",
|
||||
Namespace: "test",
|
||||
Name: "backend",
|
||||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test authorizer not allowed resource check one incorrect field",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
|
||||
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.False,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: "apps",
|
||||
Resource: "deployments-xxxx",
|
||||
Subresource: "status",
|
||||
Namespace: "test",
|
||||
Name: "backend",
|
||||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test authorizer reason",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: denyAll,
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow path check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.path('/healthz').check('get').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
Path: "/healthz",
|
||||
Verb: "get",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test authorizer decision is denied path check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.path('/healthz').check('get').allowed() == false",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: denyAll,
|
||||
},
|
||||
{
|
||||
name: "test request resource authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: endpointCreateAttributes(),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: "",
|
||||
Resource: "endpoints",
|
||||
Subresource: "",
|
||||
Namespace: "default",
|
||||
Name: "endpoints1",
|
||||
Verb: "custom-verb",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test subresource request resource authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: endpointStatusUpdateAttributes(),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: "",
|
||||
Resource: "endpoints",
|
||||
Subresource: "status",
|
||||
Namespace: "default",
|
||||
Name: "endpoints1",
|
||||
Verb: "custom-verb",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test serviceAccount authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: endpointCreateAttributes(),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "system:serviceaccount:default:test-serviceaccount",
|
||||
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default"},
|
||||
},
|
||||
ResourceRequest: true,
|
||||
APIGroup: "",
|
||||
Resource: "endpoints",
|
||||
Namespace: "default",
|
||||
Name: "endpoints1",
|
||||
Verb: "custom-verb",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := filterCompiler{}
|
||||
f := c.Compile(tc.validations, tc.hasParamKind)
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil})
|
||||
if f == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
@ -435,7 +634,8 @@ func TestFilter(t *testing.T) {
|
||||
t.Fatalf("unexpected error on conversion: %v", err)
|
||||
}
|
||||
|
||||
evalResults, err := f.ForInput(versionedAttr, tc.params, CreateAdmissionRequest(versionedAttr.Attributes))
|
||||
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||
evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@ -582,3 +782,74 @@ func TestCompilationErrors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var denyAll = fakeAuthorizer{defaultResult: authorizerResult{decision: authorizer.DecisionDeny, reason: "fake reason", err: nil}}
|
||||
|
||||
func newAuthzAllowMatch(match authorizer.AttributesRecord) fakeAuthorizer {
|
||||
return fakeAuthorizer{
|
||||
match: &authorizerMatch{
|
||||
match: match,
|
||||
authorizerResult: authorizerResult{decision: authorizer.DecisionAllow, reason: "", err: nil},
|
||||
},
|
||||
defaultResult: authorizerResult{decision: authorizer.DecisionDeny, reason: "", err: nil},
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
match *authorizerMatch
|
||||
defaultResult authorizerResult
|
||||
}
|
||||
|
||||
type authorizerResult struct {
|
||||
decision authorizer.Decision
|
||||
reason string
|
||||
err error
|
||||
}
|
||||
|
||||
type authorizerMatch struct {
|
||||
authorizerResult
|
||||
match authorizer.AttributesRecord
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.match != nil {
|
||||
other, ok := a.(*authorizer.AttributesRecord)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unsupported type: %T", a))
|
||||
}
|
||||
if reflect.DeepEqual(f.match.match, *other) {
|
||||
return f.match.decision, f.match.reason, f.match.err
|
||||
}
|
||||
}
|
||||
return f.defaultResult.decision, f.defaultResult.reason, f.defaultResult.err
|
||||
}
|
||||
|
||||
func endpointCreateAttributes() admission.Attributes {
|
||||
name := "endpoints1"
|
||||
namespace := "default"
|
||||
var object, oldObject runtime.Object
|
||||
object = &corev1.Endpoints{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Endpoints",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Subsets: []corev1.EndpointSubset{
|
||||
{
|
||||
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"}
|
||||
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"}
|
||||
return admission.NewAttributesRecord(object, oldObject, gvk, namespace, name, gvr, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||
}
|
||||
|
||||
func endpointStatusUpdateAttributes() admission.Attributes {
|
||||
attrs := endpointCreateAttributes()
|
||||
return admission.NewAttributesRecord(
|
||||
attrs.GetObject(), attrs.GetObject(), attrs.GetKind(), attrs.GetNamespace(), attrs.GetName(),
|
||||
attrs.GetResource(), "status", admission.Update, &metav1.UpdateOptions{}, false, nil)
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
v1 "k8s.io/api/admission/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
var _ ExpressionAccessor = &MatchCondition{}
|
||||
@ -49,19 +50,41 @@ func (v *MatchCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// OptionalVariableDeclarations declares which optional CEL variables
|
||||
// are declared for an expression.
|
||||
type OptionalVariableDeclarations struct {
|
||||
// HasParams specifies if the "params" variable is declared.
|
||||
// The "params" variable may still be bound to "null" when declared.
|
||||
HasParams bool
|
||||
// HasAuthorizer specifies if the"authorizer" and "authorizer.requestResource"
|
||||
// variables are declared. When declared, the authorizer variables are
|
||||
// expected to be non-null.
|
||||
HasAuthorizer bool
|
||||
}
|
||||
|
||||
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||
type FilterCompiler interface {
|
||||
// Compile is used for the cel expression compilation
|
||||
Compile(expressions []ExpressionAccessor, hasParam bool) Filter
|
||||
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations) Filter
|
||||
}
|
||||
|
||||
// OptionalVariableBindings provides expression bindings for optional CEL variables.
|
||||
type OptionalVariableBindings struct {
|
||||
// VersionedParams provides the "params" variable binding. This variable binding may
|
||||
// be set to nil even when OptionalVariableDeclarations.HashParams is set to true.
|
||||
VersionedParams runtime.Object
|
||||
// Authorizer provides the authorizer used for the "authorizer" and
|
||||
// "authorizer.requestResource" variable bindings. If the expression was compiled with
|
||||
// OptionalVariableDeclarations.HasAuthorizer set to true this must be non-nil.
|
||||
Authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
// Filter contains a function to evaluate compiled CEL-typed values
|
||||
// It expects the inbound object to already have been converted to the version expected
|
||||
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||
// versionedParams may be nil.
|
||||
type Filter interface {
|
||||
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
|
||||
ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *v1.AdmissionRequest) ([]EvaluationResult, error)
|
||||
ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings) ([]EvaluationResult, error)
|
||||
|
||||
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
||||
CompilationErrors() []error
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/component-base/featuregate"
|
||||
@ -71,6 +72,7 @@ type celAdmissionPlugin struct {
|
||||
restMapper meta.RESTMapper
|
||||
dynamicClient dynamic.Interface
|
||||
stopCh <-chan struct{}
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
|
||||
@ -78,6 +80,7 @@ var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
|
||||
|
||||
var _ admission.InitializationValidator = &celAdmissionPlugin{}
|
||||
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
||||
@ -108,6 +111,10 @@ func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
|
||||
c.stopCh = stopCh
|
||||
}
|
||||
|
||||
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
|
||||
c.authorizer = authorizer
|
||||
}
|
||||
|
||||
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
|
||||
c.enabled = true
|
||||
@ -138,7 +145,10 @@ func (c *celAdmissionPlugin) ValidateInitialization() error {
|
||||
if c.stopCh == nil {
|
||||
return errors.New("missing stop channel")
|
||||
}
|
||||
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient)
|
||||
if c.authorizer == nil {
|
||||
return errors.New("missing authorizer")
|
||||
}
|
||||
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
|
||||
if err := c.evaluator.ValidateInitialization(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
@ -42,6 +44,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/informers"
|
||||
@ -49,7 +52,6 @@ import (
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/component-base/featuregate"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -158,7 +160,7 @@ var (
|
||||
// Interface which has fake compile functionality for use in tests
|
||||
// So that we can test the controller without pulling in any CEL functionality
|
||||
type fakeCompiler struct {
|
||||
CompileFuncs map[string]func([]cel.ExpressionAccessor, bool) cel.Filter
|
||||
CompileFuncs map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter
|
||||
}
|
||||
|
||||
var _ cel.FilterCompiler = &fakeCompiler{}
|
||||
@ -169,22 +171,22 @@ func (f *fakeCompiler) HasSynced() bool {
|
||||
|
||||
func (f *fakeCompiler) Compile(
|
||||
expressions []cel.ExpressionAccessor,
|
||||
hasParam bool,
|
||||
options cel.OptionalVariableDeclarations,
|
||||
) cel.Filter {
|
||||
key := expressions[0].GetExpression()
|
||||
if fun, ok := f.CompileFuncs[key]; ok {
|
||||
return fun(expressions, hasParam)
|
||||
return fun(expressions, options)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, bool) cel.Filter) {
|
||||
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) {
|
||||
//Key must be something that we can decipher from the inputs to Validate so using expression which will be passed to validate on the filter
|
||||
key := definition.Spec.Validations[0].Expression
|
||||
if compileFunc != nil {
|
||||
if f.CompileFuncs == nil {
|
||||
f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, bool) cel.Filter)
|
||||
f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter)
|
||||
}
|
||||
f.CompileFuncs[key] = compileFunc
|
||||
}
|
||||
@ -206,7 +208,7 @@ type fakeFilter struct {
|
||||
keyId string
|
||||
}
|
||||
|
||||
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
|
||||
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
|
||||
return []cel.EvaluationResult{}, nil
|
||||
}
|
||||
|
||||
@ -333,6 +335,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
||||
testContext, testContextCancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(testContextCancel)
|
||||
|
||||
fakeAuthorizer := fakeAuthorizer{}
|
||||
dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
|
||||
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
@ -355,7 +358,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
||||
handler := plug.(*celAdmissionPlugin)
|
||||
handler.enabled = true
|
||||
|
||||
genericInitializer := initializer.New(fakeClient, dynamicClient, fakeInformerFactory, nil, featureGate, testContext.Done())
|
||||
genericInitializer := initializer.New(fakeClient, dynamicClient, fakeInformerFactory, fakeAuthorizer, featureGate, testContext.Done())
|
||||
genericInitializer.Initialize(handler)
|
||||
handler.SetRESTMapper(fakeRestMapper)
|
||||
err = admission.ValidateInitialization(handler)
|
||||
@ -365,7 +368,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
||||
// Override compiler used by controller for tests
|
||||
controller = handler.evaluator.(*celAdmissionController)
|
||||
controller.policyController.filterCompiler = compiler
|
||||
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType) Validator {
|
||||
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||
f := filter.(*fakeFilter)
|
||||
v := validatorMap[f.keyId]
|
||||
v.fakeFilter = f
|
||||
@ -702,7 +705,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
||||
DefaultMatch: true,
|
||||
}
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -762,7 +765,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
||||
passedParams := []*unstructured.Unstructured{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -873,7 +876,7 @@ func TestReconfigureBinding(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -980,7 +983,7 @@ func TestRemoveDefinition(t *testing.T) {
|
||||
datalock := sync.Mutex{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -1047,7 +1050,7 @@ func TestRemoveBinding(t *testing.T) {
|
||||
datalock := sync.Mutex{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -1155,7 +1158,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
|
||||
passedParams := []*unstructured.Unstructured{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -1221,7 +1224,7 @@ func TestEmptyParamSource(t *testing.T) {
|
||||
noParamSourcePolicy := *denyPolicy
|
||||
noParamSourcePolicy.Spec.ParamKind = nil
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
@ -1323,7 +1326,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
||||
compiles2 := atomic.Int64{}
|
||||
evaluations2 := atomic.Int64{}
|
||||
|
||||
compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
compiles1.Add(1)
|
||||
|
||||
return &fakeFilter{
|
||||
@ -1340,7 +1343,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
compiles2.Add(1)
|
||||
|
||||
return &fakeFilter{
|
||||
@ -1448,7 +1451,7 @@ func TestNativeTypeParam(t *testing.T) {
|
||||
Kind: "ConfigMap",
|
||||
}
|
||||
|
||||
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
compiles.Add(1)
|
||||
|
||||
return &fakeFilter{
|
||||
@ -1511,3 +1514,9 @@ func TestNativeTypeParam(t *testing.T) {
|
||||
require.EqualValues(t, 1, compiles.Load())
|
||||
require.EqualValues(t, 1, evaluations.Load())
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct{}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@ -119,6 +120,7 @@ func NewAdmissionController(
|
||||
client kubernetes.Interface,
|
||||
restMapper meta.RESTMapper,
|
||||
dynamicClient dynamic.Interface,
|
||||
authz authorizer.Authorizer,
|
||||
) CELPolicyEvaluator {
|
||||
return &celAdmissionController{
|
||||
definitions: atomic.Value{},
|
||||
@ -132,6 +134,7 @@ func NewAdmissionController(
|
||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
||||
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
|
||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
|
||||
authz,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||
"k8s.io/client-go/informers"
|
||||
@ -85,9 +86,11 @@ type policyController struct {
|
||||
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
||||
|
||||
client kubernetes.Interface
|
||||
|
||||
authz authorizer.Authorizer
|
||||
}
|
||||
|
||||
type newValidator func(cel.Filter, *v1.FailurePolicyType) Validator
|
||||
type newValidator func(cel.Filter, *v1.FailurePolicyType, authorizer.Authorizer) Validator
|
||||
|
||||
func newPolicyController(
|
||||
restMapper meta.RESTMapper,
|
||||
@ -97,6 +100,7 @@ func newPolicyController(
|
||||
matcher Matcher,
|
||||
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
|
||||
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
|
||||
authz authorizer.Authorizer,
|
||||
) *policyController {
|
||||
res := &policyController{}
|
||||
*res = policyController{
|
||||
@ -126,6 +130,7 @@ func newPolicyController(
|
||||
restMapper: restMapper,
|
||||
dynamicClient: dynamicClient,
|
||||
client: client,
|
||||
authz: authz,
|
||||
}
|
||||
return res
|
||||
}
|
||||
@ -439,9 +444,11 @@ func (c *policyController) latestPolicyData() []policyData {
|
||||
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
|
||||
hasParam = true
|
||||
}
|
||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||
bindingInfo.validator = c.newValidator(
|
||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), hasParam),
|
||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars),
|
||||
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
||||
c.authz,
|
||||
)
|
||||
}
|
||||
bindingInfos = append(bindingInfos, *bindingInfo)
|
||||
|
@ -18,6 +18,7 @@ package validatingadmissionpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
|
@ -18,28 +18,31 @@ package validatingadmissionpolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/klog/v2"
|
||||
"strings"
|
||||
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// validator implements the Validator interface
|
||||
type validator struct {
|
||||
filter cel.Filter
|
||||
failPolicy *v1.FailurePolicyType
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
|
||||
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||
return &validator{
|
||||
filter: filter,
|
||||
failPolicy: failPolicy,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +62,8 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
|
||||
f = *v.failPolicy
|
||||
}
|
||||
|
||||
evalResults, err := v.filter.ForInput(versionedAttr, versionedParams, cel.CreateAdmissionRequest(versionedAttr.Attributes))
|
||||
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
|
||||
evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
|
||||
if err != nil {
|
||||
return []PolicyDecision{
|
||||
{
|
||||
|
@ -28,7 +28,6 @@ import (
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
@ -42,7 +41,7 @@ type fakeCelFilter struct {
|
||||
throwError bool
|
||||
}
|
||||
|
||||
func (f *fakeCelFilter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
|
||||
func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
|
||||
if f.throwError {
|
||||
return nil, errors.New("test error")
|
||||
}
|
||||
@ -465,6 +464,7 @@ func TestValidate(t *testing.T) {
|
||||
throwError: tc.throwError,
|
||||
},
|
||||
}
|
||||
|
||||
policyResults := v.Validate(fakeVersionedAttr, nil)
|
||||
|
||||
require.Equal(t, len(policyResults), len(tc.policyDecision))
|
||||
|
580
staging/src/k8s.io/apiserver/pkg/cel/library/authz.go
Normal file
580
staging/src/k8s.io/apiserver/pkg/cel/library/authz.go
Normal file
@ -0,0 +1,580 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package library
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// Authz provides a CEL function library extension for performing authorization checks.
|
||||
// Note that authorization checks are only supported for CEL expression fields in the API
|
||||
// where an 'authorizer' variable is provided to the CEL expression. See the
|
||||
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
|
||||
// variable is provided.
|
||||
//
|
||||
// path
|
||||
//
|
||||
// Returns a PathCheck configured to check authorization for a non-resource request
|
||||
// path (e.g. /healthz). If path is an empty string, an error is returned.
|
||||
// Note that the leading '/' is not required.
|
||||
//
|
||||
// <Authorizer>.path(<string>) <PathCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.path('/healthz') // returns a PathCheck for the '/healthz' API path
|
||||
// authorizer.path('') // results in "path must not be empty" error
|
||||
// authorizer.path(' ') // results in "path must not be empty" error
|
||||
//
|
||||
// group
|
||||
//
|
||||
// Returns a GroupCheck configured to check authorization for the API resources for
|
||||
// a particular API group.
|
||||
// Note that authorization checks are only supported for CEL expression fields in the API
|
||||
// where an 'authorizer' variable is provided to the CEL expression. Check the
|
||||
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
|
||||
// variable is provided.
|
||||
//
|
||||
// <Authorizer>.group(<string>) <GroupCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('apps') // returns a GroupCheck for the 'apps' API group
|
||||
// authorizer.group('') // returns a GroupCheck for the core API group
|
||||
// authorizer.group('example.com') // returns a GroupCheck for the custom resources in the 'example.com' API group
|
||||
//
|
||||
// serviceAccount
|
||||
//
|
||||
// Returns an Authorizer configured to check authorization for the provided service account namespace and name.
|
||||
// If the name is not a valid DNS subdomain string (as defined by RFC 1123), an error is returned.
|
||||
// If the namespace is not a valid DNS label (as defined by RFC 1123), an error is returned.
|
||||
//
|
||||
// <Authorizer>.serviceAccount(<string>, <string>) <Authorizer>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.serviceAccount('default', 'myserviceaccount') // returns an Authorizer for the service account with namespace 'default' and name 'myserviceaccount'
|
||||
// authorizer.serviceAccount('not@a#valid!namespace', 'validname') // returns an error
|
||||
// authorizer.serviceAccount('valid.example.com', 'invalid@*name') // returns an error
|
||||
//
|
||||
// resource
|
||||
//
|
||||
// Returns a ResourceCheck configured to check authorization for a particular API resource.
|
||||
// Note that the provided resource string should be a lower case plural name of a Kubernetes API resource.
|
||||
//
|
||||
// <GroupCheck>.resource(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('apps').resource('deployments') // returns a ResourceCheck for the 'deployments' resources in the 'apps' group.
|
||||
// authorizer.group('').resource('pods') // returns a ResourceCheck for the 'pods' resources in the core group.
|
||||
// authorizer.group('apps').resource('') // results in "resource must not be empty" error
|
||||
// authorizer.group('apps').resource(' ') // results in "resource must not be empty" error
|
||||
//
|
||||
// subresource
|
||||
//
|
||||
// Returns a ResourceCheck configured to check authorization for a particular subresource of an API resource.
|
||||
// If subresource is set to "", the subresource field of this ResourceCheck is considered unset.
|
||||
//
|
||||
// <ResourceCheck>.subresource(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').subresource('status') // returns a ResourceCheck the 'status' subresource of 'pods'
|
||||
// authorizer.group('apps').resource('deployments').subresource('scale') // returns a ResourceCheck the 'scale' subresource of 'deployments'
|
||||
// authorizer.group('example.com').resource('widgets').subresource('scale') // returns a ResourceCheck for the 'scale' subresource of the 'widgets' custom resource
|
||||
// authorizer.group('example.com').resource('widgets').subresource('') // returns a ResourceCheck for the 'widgets' resource.
|
||||
//
|
||||
// namespace
|
||||
//
|
||||
// Returns a ResourceCheck configured to check authorization for a particular namespace.
|
||||
// For cluster scoped resources, namespace() does not need to be called; namespace defaults
|
||||
// to "", which is the correct namespace value to use to check cluster scoped resources.
|
||||
// If namespace is set to "", the ResourceCheck will check authorization for the cluster scope.
|
||||
//
|
||||
// <ResourceCheck>.namespace(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('apps').resource('deployments').namespace('test') // returns a ResourceCheck for 'deployments' in the 'test' namespace
|
||||
// authorizer.group('').resource('pods').namespace('default') // returns a ResourceCheck for 'pods' in the 'default' namespace
|
||||
// authorizer.group('').resource('widgets').namespace('') // returns a ResourceCheck for 'widgets' in the cluster scope
|
||||
//
|
||||
// name
|
||||
//
|
||||
// Returns a ResourceCheck configured to check authorization for a particular resource name.
|
||||
// If name is set to "", the name field of this ResourceCheck is considered unset.
|
||||
//
|
||||
// <ResourceCheck>.name(<name>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('apps').resource('deployments').namespace('test').name('backend') // returns a ResourceCheck for the 'backend' 'deployments' resource in the 'test' namespace
|
||||
// authorizer.group('apps').resource('deployments').namespace('test').name('') // returns a ResourceCheck for the 'deployments' resource in the 'test' namespace
|
||||
//
|
||||
// check
|
||||
//
|
||||
// For PathCheck, checks if the principal (user or service account) that sent the request is authorized for the HTTP request verb of the path.
|
||||
// For ResourceCheck, checks if the principal (user or service account) that sent the request is authorized for the API verb and the configured authorization checks of the ResourceCheck.
|
||||
// The check operation can be expensive, particularly in clusters using the webhook authorization mode.
|
||||
//
|
||||
// <PathCheck>.check(<check>) <Decision>
|
||||
// <ResourceCheck>.check(<check>) <Decision>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').namespace('default').check('create') // Checks if the principal (user or service account) is authorized create pods in the 'default' namespace.
|
||||
// authorizer.path('/healthz').check('get') // Checks if the principal (user or service account) is authorized to make HTTP GET requests to the /healthz API path.
|
||||
//
|
||||
// allowed
|
||||
//
|
||||
// Returns true if the authorizer's decision for the check is "allow". Note that if the authorizer's decision is
|
||||
// "no opinion", that both the 'allowed' and 'denied' functions will return false.
|
||||
//
|
||||
// <Decision>.allowed() <bool>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').namespace('default').check('create').allowed() // Returns true if the principal (user or service account) is allowed create pods in the 'default' namespace.
|
||||
// authorizer.path('/healthz').check('get').allowed() // Returns true if the principal (user or service account) is allowed to make HTTP GET requests to the /healthz API path.
|
||||
//
|
||||
// reason
|
||||
//
|
||||
// Returns a string reason for the authorization decision
|
||||
//
|
||||
// <Decision>.reason() <string>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.path('/healthz').check('GET').reason()
|
||||
func Authz() cel.EnvOption {
|
||||
return cel.Lib(authzLib)
|
||||
}
|
||||
|
||||
var authzLib = &authz{}
|
||||
|
||||
type authz struct{}
|
||||
|
||||
var authzLibraryDecls = map[string][]cel.FunctionOpt{
|
||||
"path": {
|
||||
cel.MemberOverload("authorizer_path", []*cel.Type{AuthorizerType, cel.StringType}, PathCheckType,
|
||||
cel.BinaryBinding(authorizerPath))},
|
||||
"group": {
|
||||
cel.MemberOverload("authorizer_group", []*cel.Type{AuthorizerType, cel.StringType}, GroupCheckType,
|
||||
cel.BinaryBinding(authorizerGroup))},
|
||||
"serviceAccount": {
|
||||
cel.MemberOverload("authorizer_serviceaccount", []*cel.Type{AuthorizerType, cel.StringType, cel.StringType}, AuthorizerType,
|
||||
cel.FunctionBinding(authorizerServiceAccount))},
|
||||
"resource": {
|
||||
cel.MemberOverload("groupcheck_resource", []*cel.Type{GroupCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(groupCheckResource))},
|
||||
"subresource": {
|
||||
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckSubresource))},
|
||||
"namespace": {
|
||||
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckNamespace))},
|
||||
"name": {
|
||||
cel.MemberOverload("resourcecheck_name", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckName))},
|
||||
"check": {
|
||||
cel.MemberOverload("pathcheck_check", []*cel.Type{PathCheckType, cel.StringType}, DecisionType,
|
||||
cel.BinaryBinding(pathCheckCheck)),
|
||||
cel.MemberOverload("resourcecheck_check", []*cel.Type{ResourceCheckType, cel.StringType}, DecisionType,
|
||||
cel.BinaryBinding(resourceCheckCheck))},
|
||||
"allowed": {
|
||||
cel.MemberOverload("decision_allowed", []*cel.Type{DecisionType}, cel.BoolType,
|
||||
cel.UnaryBinding(decisionAllowed))},
|
||||
"reason": {
|
||||
cel.MemberOverload("decision_reason", []*cel.Type{DecisionType}, cel.StringType,
|
||||
cel.UnaryBinding(decisionReason))},
|
||||
}
|
||||
|
||||
func (*authz) CompileOptions() []cel.EnvOption {
|
||||
options := make([]cel.EnvOption, 0, len(authzLibraryDecls))
|
||||
for name, overloads := range authzLibraryDecls {
|
||||
options = append(options, cel.Function(name, overloads...))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*authz) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
|
||||
authz, ok := arg1.(authorizerVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
path, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(path)) == 0 {
|
||||
return types.NewErr("path must not be empty")
|
||||
}
|
||||
|
||||
return authz.pathCheck(path)
|
||||
}
|
||||
|
||||
func authorizerGroup(arg1, arg2 ref.Val) ref.Val {
|
||||
authz, ok := arg1.(authorizerVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
group, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
return authz.groupCheck(group)
|
||||
}
|
||||
|
||||
func authorizerServiceAccount(args ...ref.Val) ref.Val {
|
||||
argn := len(args)
|
||||
if argn != 3 {
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
|
||||
authz, ok := args[0].(authorizerVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(args[0])
|
||||
}
|
||||
|
||||
namespace, ok := args[1].Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(args[1])
|
||||
}
|
||||
|
||||
name, ok := args[2].Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(args[2])
|
||||
}
|
||||
|
||||
if errors := apimachineryvalidation.ValidateServiceAccountName(name, false); len(errors) > 0 {
|
||||
return types.NewErr("Invalid service account name")
|
||||
}
|
||||
if errors := apimachineryvalidation.ValidateNamespaceName(namespace, false); len(errors) > 0 {
|
||||
return types.NewErr("Invalid service account namespace")
|
||||
}
|
||||
return authz.serviceAccount(namespace, name)
|
||||
}
|
||||
|
||||
func groupCheckResource(arg1, arg2 ref.Val) ref.Val {
|
||||
groupCheck, ok := arg1.(groupCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
resource, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(resource)) == 0 {
|
||||
return types.NewErr("resource must not be empty")
|
||||
}
|
||||
return groupCheck.resourceCheck(resource)
|
||||
}
|
||||
|
||||
func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val {
|
||||
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
subresource, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
result := resourceCheck
|
||||
result.subresource = subresource
|
||||
return result
|
||||
}
|
||||
|
||||
func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val {
|
||||
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
namespace, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
result := resourceCheck
|
||||
result.namespace = namespace
|
||||
return result
|
||||
}
|
||||
|
||||
func resourceCheckName(arg1, arg2 ref.Val) ref.Val {
|
||||
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
name, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
result := resourceCheck
|
||||
result.name = name
|
||||
return result
|
||||
}
|
||||
|
||||
func pathCheckCheck(arg1, arg2 ref.Val) ref.Val {
|
||||
pathCheck, ok := arg1.(pathCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
httpRequestVerb, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
return pathCheck.Authorize(context.TODO(), httpRequestVerb)
|
||||
}
|
||||
|
||||
func resourceCheckCheck(arg1, arg2 ref.Val) ref.Val {
|
||||
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
apiVerb, ok := arg2.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
return resourceCheck.Authorize(context.TODO(), apiVerb)
|
||||
}
|
||||
|
||||
func decisionAllowed(arg ref.Val) ref.Val {
|
||||
decision, ok := arg.(decisionVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
return types.Bool(decision.authDecision == authorizer.DecisionAllow)
|
||||
}
|
||||
|
||||
func decisionReason(arg ref.Val) ref.Val {
|
||||
decision, ok := arg.(decisionVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
return types.String(decision.reason)
|
||||
}
|
||||
|
||||
var (
|
||||
AuthorizerType = cel.ObjectType("kubernetes.authorization.Authorizer")
|
||||
PathCheckType = cel.ObjectType("kubernetes.authorization.PathCheck")
|
||||
GroupCheckType = cel.ObjectType("kubernetes.authorization.GroupCheck")
|
||||
ResourceCheckType = cel.ObjectType("kubernetes.authorization.ResourceCheck")
|
||||
DecisionType = cel.ObjectType("kubernetes.authorization.Decision")
|
||||
)
|
||||
|
||||
// Resource represents an API resource
|
||||
type Resource interface {
|
||||
// GetName returns the name of the object as presented in the request. On a CREATE operation, the client
|
||||
// may omit name and rely on the server to generate the name. If that is the case, this method will return
|
||||
// the empty string
|
||||
GetName() string
|
||||
// GetNamespace is the namespace associated with the request (if any)
|
||||
GetNamespace() string
|
||||
// GetResource is the name of the resource being requested. This is not the kind. For example: pods
|
||||
GetResource() schema.GroupVersionResource
|
||||
// GetSubresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
|
||||
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
|
||||
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
|
||||
GetSubresource() string
|
||||
}
|
||||
|
||||
func NewAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer) ref.Val {
|
||||
return authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
|
||||
}
|
||||
|
||||
func NewResourceAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer, requestResource Resource) ref.Val {
|
||||
a := authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
|
||||
resource := requestResource.GetResource()
|
||||
g := a.groupCheck(resource.Group)
|
||||
r := g.resourceCheck(resource.Resource)
|
||||
r.subresource = requestResource.GetSubresource()
|
||||
r.namespace = requestResource.GetNamespace()
|
||||
r.name = requestResource.GetName()
|
||||
return r
|
||||
}
|
||||
|
||||
type authorizerVal struct {
|
||||
receiverOnlyObjectVal
|
||||
userInfo user.Info
|
||||
authAuthorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
func (a authorizerVal) pathCheck(path string) pathCheckVal {
|
||||
return pathCheckVal{receiverOnlyObjectVal: receiverOnlyVal(PathCheckType), authorizer: a, path: path}
|
||||
}
|
||||
|
||||
func (a authorizerVal) groupCheck(group string) groupCheckVal {
|
||||
return groupCheckVal{receiverOnlyObjectVal: receiverOnlyVal(GroupCheckType), authorizer: a, group: group}
|
||||
}
|
||||
|
||||
func (a authorizerVal) serviceAccount(namespace, name string) authorizerVal {
|
||||
sa := &serviceaccount.ServiceAccountInfo{Name: name, Namespace: namespace}
|
||||
return authorizerVal{
|
||||
receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType),
|
||||
userInfo: sa.UserInfo(),
|
||||
authAuthorizer: a.authAuthorizer,
|
||||
}
|
||||
}
|
||||
|
||||
type pathCheckVal struct {
|
||||
receiverOnlyObjectVal
|
||||
authorizer authorizerVal
|
||||
path string
|
||||
}
|
||||
|
||||
func (a pathCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
|
||||
attr := &authorizer.AttributesRecord{
|
||||
Path: a.path,
|
||||
Verb: verb,
|
||||
User: a.authorizer.userInfo,
|
||||
}
|
||||
|
||||
decision, reason, err := a.authorizer.authAuthorizer.Authorize(ctx, attr)
|
||||
if err != nil {
|
||||
return types.NewErr("error in authorization check: %v", err)
|
||||
}
|
||||
return newDecision(decision, reason)
|
||||
}
|
||||
|
||||
type groupCheckVal struct {
|
||||
receiverOnlyObjectVal
|
||||
authorizer authorizerVal
|
||||
group string
|
||||
}
|
||||
|
||||
func (g groupCheckVal) resourceCheck(resource string) resourceCheckVal {
|
||||
return resourceCheckVal{receiverOnlyObjectVal: receiverOnlyVal(ResourceCheckType), groupCheck: g, resource: resource}
|
||||
}
|
||||
|
||||
type resourceCheckVal struct {
|
||||
receiverOnlyObjectVal
|
||||
groupCheck groupCheckVal
|
||||
resource string
|
||||
subresource string
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
|
||||
func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
|
||||
attr := &authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: a.groupCheck.group,
|
||||
APIVersion: "*",
|
||||
Resource: a.resource,
|
||||
Subresource: a.subresource,
|
||||
Namespace: a.namespace,
|
||||
Name: a.name,
|
||||
Verb: verb,
|
||||
User: a.groupCheck.authorizer.userInfo,
|
||||
}
|
||||
decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr)
|
||||
if err != nil {
|
||||
return types.NewErr("error in authorization check: %v", err)
|
||||
}
|
||||
return newDecision(decision, reason)
|
||||
}
|
||||
|
||||
func newDecision(authDecision authorizer.Decision, reason string) decisionVal {
|
||||
return decisionVal{receiverOnlyObjectVal: receiverOnlyVal(DecisionType), authDecision: authDecision, reason: reason}
|
||||
}
|
||||
|
||||
type decisionVal struct {
|
||||
receiverOnlyObjectVal
|
||||
authDecision authorizer.Decision
|
||||
reason string
|
||||
}
|
||||
|
||||
// receiverOnlyObjectVal provides an implementation of ref.Val for
|
||||
// any object type that has receiver functions but does not expose any fields to
|
||||
// CEL.
|
||||
type receiverOnlyObjectVal struct {
|
||||
typeValue *types.TypeValue
|
||||
}
|
||||
|
||||
// receiverOnlyVal returns a receiverOnlyObjectVal for the given type.
|
||||
func receiverOnlyVal(objectType *cel.Type) receiverOnlyObjectVal {
|
||||
return receiverOnlyObjectVal{typeValue: types.NewTypeValue(objectType.String())}
|
||||
}
|
||||
|
||||
// ConvertToNative implements ref.Val.ConvertToNative.
|
||||
func (a receiverOnlyObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return nil, fmt.Errorf("type conversion error from '%s' to '%v'", a.typeValue.String(), typeDesc)
|
||||
}
|
||||
|
||||
// ConvertToType implements ref.Val.ConvertToType.
|
||||
func (a receiverOnlyObjectVal) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
switch typeVal {
|
||||
case a.typeValue:
|
||||
return a
|
||||
case types.TypeType:
|
||||
return a.typeValue
|
||||
}
|
||||
return types.NewErr("type conversion error from '%s' to '%s'", a.typeValue, typeVal)
|
||||
}
|
||||
|
||||
// Equal implements ref.Val.Equal.
|
||||
func (a receiverOnlyObjectVal) Equal(other ref.Val) ref.Val {
|
||||
o, ok := other.(receiverOnlyObjectVal)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(other)
|
||||
}
|
||||
return types.Bool(a == o)
|
||||
}
|
||||
|
||||
// Type implements ref.Val.Type.
|
||||
func (a receiverOnlyObjectVal) Type() ref.Type {
|
||||
return a.typeValue
|
||||
}
|
||||
|
||||
// Value implements ref.Val.Value.
|
||||
func (a receiverOnlyObjectVal) Value() any {
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
@ -36,6 +36,15 @@ type CostEstimator struct {
|
||||
|
||||
func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 {
|
||||
switch function {
|
||||
case "check":
|
||||
// An authorization check has a fixed cost
|
||||
// This cost is set to allow for only two authorization checks per expression
|
||||
cost := uint64(350000)
|
||||
return &cost
|
||||
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
|
||||
// All authorization builder and accessor functions have a nominal cost
|
||||
cost := uint64(1)
|
||||
return &cost
|
||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||
var cost uint64
|
||||
if len(args) > 0 {
|
||||
@ -78,6 +87,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
||||
// WARNING: Any changes to this code impact API compatibility! The estimated cost is used to determine which CEL rules may be written to a
|
||||
// CRD and any change (cost increases and cost decreases) are breaking.
|
||||
switch function {
|
||||
case "check":
|
||||
// An authorization check has a fixed cost
|
||||
// This cost is set to allow for only two authorization checks per expression
|
||||
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 350000, Max: 350000}}
|
||||
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
|
||||
// All authorization builder and accessor functions have a nominal cost
|
||||
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
|
||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||
if target != nil {
|
||||
// Charge 1 cost for comparing each element in the list
|
||||
@ -94,7 +110,6 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
||||
} else { // the target is a string, which is supported by indexOf and lastIndexOf
|
||||
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
|
||||
}
|
||||
|
||||
}
|
||||
case "url":
|
||||
if len(args) == 1 {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package library
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@ -24,6 +25,8 @@ import (
|
||||
"github.com/google/cel-go/checker"
|
||||
"github.com/google/cel-go/ext"
|
||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -311,12 +314,62 @@ func TestStringLibrary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthzLibrary(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
expectEstimatedCost checker.CostEstimate
|
||||
expectRuntimeCost uint64
|
||||
}{
|
||||
{
|
||||
name: "path",
|
||||
expr: "authorizer.path('/healthz')",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
||||
expectRuntimeCost: 2,
|
||||
},
|
||||
{
|
||||
name: "resource",
|
||||
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend')",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
||||
expectRuntimeCost: 6,
|
||||
},
|
||||
{
|
||||
name: "path check allowed",
|
||||
expr: "authorizer.path('/healthz').check('get').allowed()",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 350003, Max: 350003},
|
||||
expectRuntimeCost: 350003,
|
||||
},
|
||||
{
|
||||
name: "resource check allowed",
|
||||
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
||||
expectRuntimeCost: 350007,
|
||||
},
|
||||
{
|
||||
name: "resource check reason",
|
||||
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
||||
expectRuntimeCost: 350007,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
|
||||
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
||||
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
env, err = env.Extend(cel.Variable("authorizer", AuthorizerType))
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
compiled, issues := env.Compile(expr)
|
||||
if len(issues.Errors()) > 0 {
|
||||
t.Fatalf("%v", issues.Errors())
|
||||
@ -332,7 +385,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
_, details, err := prog.Eval(map[string]interface{}{})
|
||||
_, details, err := prog.Eval(map[string]interface{}{"authorizer": NewAuthorizerVal(nil, alwaysAllowAuthorizer{})})
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@ -361,3 +414,9 @@ func (t *testCostEstimator) EstimateSize(element checker.AstNode) *checker.SizeE
|
||||
func (t *testCostEstimator) EstimateCallCost(function, overloadId string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
|
||||
return nil
|
||||
}
|
||||
|
||||
type alwaysAllowAuthorizer struct{}
|
||||
|
||||
func (f alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ var k8sExtensionLibs = []cel.EnvOption{
|
||||
URLs(),
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
}
|
||||
|
||||
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}
|
||||
|
@ -29,6 +29,7 @@ func TestLibraryCompatibility(t *testing.T) {
|
||||
urlsLib: urlLibraryDecls,
|
||||
listsLib: listsLibraryDecls,
|
||||
regexLib: regexLibraryDecls,
|
||||
authzLib: authzLibraryDecls,
|
||||
}
|
||||
if len(k8sExtensionLibs) != len(decls) {
|
||||
t.Errorf("Expected the same number of libraries in the ExtensionLibs as are tested for compatibility")
|
||||
@ -46,6 +47,8 @@ func TestLibraryCompatibility(t *testing.T) {
|
||||
// Kubernetes 1.24:
|
||||
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
|
||||
"getPort", "getEscapedPath", "getQuery", "isURL",
|
||||
// Kubernetes <1.27>:
|
||||
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "denied", "reason",
|
||||
// Kubernetes <1.??>:
|
||||
}
|
||||
for _, fn := range knownFunctions {
|
||||
|
@ -27,8 +27,11 @@ import (
|
||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/rest"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/authutil"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
|
||||
@ -41,6 +44,7 @@ import (
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
@ -2050,6 +2054,177 @@ func TestBindingRemoval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test_ValidateSecondaryAuthorization tests a ValidatingAdmissionPolicy that performs secondary authorization checks
|
||||
// for both users and service accounts.
|
||||
func Test_ValidateSecondaryAuthorization(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
rbac *rbacv1.PolicyRule
|
||||
expression string
|
||||
allowed bool
|
||||
extraAccountFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset
|
||||
extraAccountRbac *rbacv1.PolicyRule
|
||||
}{
|
||||
{
|
||||
name: "principal is allowed to create a specific deployment",
|
||||
rbac: &rbacv1.PolicyRule{
|
||||
Verbs: []string{"create"},
|
||||
APIGroups: []string{"apps"},
|
||||
Resources: []string{"deployments/status"},
|
||||
ResourceNames: []string{"charmander"},
|
||||
},
|
||||
expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('default').namespace('default').name('charmander').check('create').allowed()",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "principal is not allowed to create a specific deployment",
|
||||
expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('default').name('charmander').check('create').allowed()",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "principal is authorized for custom verb on current resource",
|
||||
rbac: &rbacv1.PolicyRule{
|
||||
Verbs: []string{"anthropomorphize"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"namespaces"},
|
||||
},
|
||||
expression: "authorizer.requestResource.check('anthropomorphize').allowed()",
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
name: "principal is not authorized for custom verb on current resource",
|
||||
expression: "authorizer.requestResource.check('anthropomorphize').allowed()",
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
name: "serviceaccount is authorized for custom verb on current resource",
|
||||
extraAccountFn: serviceAccountClient("default", "extra-acct"),
|
||||
extraAccountRbac: &rbacv1.PolicyRule{
|
||||
Verbs: []string{"anthropomorphize"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
expression: "authorizer.serviceAccount('default', 'extra-acct').group('').resource('pods').check('anthropomorphize').allowed()",
|
||||
allowed: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
clients := map[string]func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset{
|
||||
"user": secondaryAuthorizationUserClient,
|
||||
"serviceaccount": secondaryAuthorizationServiceAccountClient,
|
||||
}
|
||||
|
||||
for clientName, clientFn := range clients {
|
||||
t.Run(clientName, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
|
||||
server, err := apiservertesting.StartTestServer(t, nil, []string{
|
||||
"--enable-admission-plugins", "ValidatingAdmissionPolicy",
|
||||
"--authorization-mode=RBAC",
|
||||
"--anonymous-auth",
|
||||
}, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
|
||||
// For test set up such as creating policies, bindings and RBAC rules.
|
||||
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
|
||||
|
||||
// Principal is always allowed to create and update namespaces so that the admission requests to test
|
||||
// authorization expressions can be sent by the principal.
|
||||
rules := []rbacv1.PolicyRule{{
|
||||
Verbs: []string{"create", "update"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"namespaces"},
|
||||
}}
|
||||
if testcase.rbac != nil {
|
||||
rules = append(rules, *testcase.rbac)
|
||||
}
|
||||
|
||||
client := clientFn(t, adminClient, server.ClientConfig, rules)
|
||||
|
||||
if testcase.extraAccountFn != nil {
|
||||
var extraRules []rbacv1.PolicyRule
|
||||
if testcase.extraAccountRbac != nil {
|
||||
extraRules = append(rules, *testcase.extraAccountRbac)
|
||||
}
|
||||
testcase.extraAccountFn(t, adminClient, server.ClientConfig, extraRules)
|
||||
}
|
||||
|
||||
policy := withWaitReadyConstraintAndExpression(withValidations([]admissionregistrationv1alpha1.Validation{
|
||||
{
|
||||
Expression: testcase.expression,
|
||||
},
|
||||
}, withFailurePolicy(admissionregistrationv1alpha1.Fail, withNamespaceMatch(makePolicy("validate-authz")))))
|
||||
if _, err := adminClient.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createAndWaitReady(t, adminClient, makeBinding("validate-authz-binding", "validate-authz", ""), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ns := &v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-authz",
|
||||
},
|
||||
}
|
||||
_, err = client.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
|
||||
|
||||
var expected metav1.StatusReason = ""
|
||||
if !testcase.allowed {
|
||||
expected = metav1.StatusReasonInvalid
|
||||
}
|
||||
checkFailureReason(t, err, expected)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type clientFn func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset
|
||||
|
||||
func secondaryAuthorizationUserClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
|
||||
clientConfig = rest.CopyConfig(clientConfig)
|
||||
clientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: "alice",
|
||||
UID: "1234",
|
||||
}
|
||||
client := clientset.NewForConfigOrDie(clientConfig)
|
||||
|
||||
for _, rule := range rules {
|
||||
authutil.GrantUserAuthorization(t, context.TODO(), adminClient, "alice", rule)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func secondaryAuthorizationServiceAccountClient(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
|
||||
return serviceAccountClient("default", "test-service-acct")(t, adminClient, clientConfig, rules)
|
||||
}
|
||||
|
||||
func serviceAccountClient(namespace, name string) clientFn {
|
||||
return func(t *testing.T, adminClient *clientset.Clientset, clientConfig *rest.Config, rules []rbacv1.PolicyRule) *clientset.Clientset {
|
||||
clientConfig = rest.CopyConfig(clientConfig)
|
||||
sa, err := adminClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uid := sa.UID
|
||||
|
||||
clientConfig.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: "system:serviceaccount:" + namespace + ":" + name,
|
||||
UID: string(uid),
|
||||
}
|
||||
client := clientset.NewForConfigOrDie(clientConfig)
|
||||
|
||||
for _, rule := range rules {
|
||||
authutil.GrantServiceAccountAuthorization(t, context.TODO(), adminClient, name, namespace, rule)
|
||||
}
|
||||
return client
|
||||
}
|
||||
}
|
||||
|
||||
func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
|
||||
policy = policy.DeepCopy()
|
||||
policy.Spec.MatchConstraints.ResourceRules = append(policy.Spec.MatchConstraints.ResourceRules, admissionregistrationv1alpha1.NamedRuleWithOperations{
|
||||
@ -2280,11 +2455,16 @@ func checkFailureReason(t *testing.T, err error, expectedReason metav1.StatusRea
|
||||
// no reason was given, no error was passed - early exit
|
||||
return
|
||||
}
|
||||
reason := err.(apierrors.APIStatus).Status().Reason
|
||||
if reason != expectedReason {
|
||||
t.Logf("actual error reason: %v", reason)
|
||||
t.Logf("expected failure reason: %v", expectedReason)
|
||||
t.Error("unexpected error reason")
|
||||
switch e := err.(type) {
|
||||
case apierrors.APIStatus:
|
||||
reason := e.Status().Reason
|
||||
if reason != expectedReason {
|
||||
t.Logf("actual error reason: %v", reason)
|
||||
t.Logf("expected failure reason: %v", expectedReason)
|
||||
t.Error("Unexpected error reason")
|
||||
}
|
||||
default:
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,14 @@ func WaitForNamedAuthorizationUpdate(t *testing.T, ctx context.Context, c author
|
||||
}
|
||||
|
||||
func GrantUserAuthorization(t *testing.T, ctx context.Context, adminClient clientset.Interface, username string, rule rbacv1.PolicyRule) {
|
||||
grantAuthorization(t, ctx, adminClient, username, "", rbacv1.UserKind, rule)
|
||||
}
|
||||
|
||||
func GrantServiceAccountAuthorization(t *testing.T, ctx context.Context, adminClient clientset.Interface, serviceAccountName, serviceAccountNamespace string, rule rbacv1.PolicyRule) {
|
||||
grantAuthorization(t, ctx, adminClient, serviceAccountName, serviceAccountNamespace, rbacv1.ServiceAccountKind, rule)
|
||||
}
|
||||
|
||||
func grantAuthorization(t *testing.T, ctx context.Context, adminClient clientset.Interface, name, namespace, accountKind string, rule rbacv1.PolicyRule) {
|
||||
t.Helper()
|
||||
|
||||
cr, err := adminClient.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{
|
||||
@ -83,10 +91,10 @@ func GrantUserAuthorization(t *testing.T, ctx context.Context, adminClient clien
|
||||
ObjectMeta: metav1.ObjectMeta{GenerateName: strings.Replace(t.Name(), "/", "--", -1)},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: username,
|
||||
Namespace: "",
|
||||
Kind: accountKind,
|
||||
// APIGroup defaults to the appropriate value for both users and service accounts
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
@ -107,12 +115,17 @@ func GrantUserAuthorization(t *testing.T, ctx context.Context, adminClient clien
|
||||
resourceName = rule.ResourceNames[0]
|
||||
}
|
||||
|
||||
subjectName := name
|
||||
if accountKind == rbacv1.ServiceAccountKind {
|
||||
subjectName = "system:serviceaccount:" + namespace + ":" + name
|
||||
}
|
||||
|
||||
WaitForNamedAuthorizationUpdate(
|
||||
t,
|
||||
ctx,
|
||||
adminClient.AuthorizationV1(),
|
||||
username,
|
||||
"",
|
||||
subjectName,
|
||||
namespace,
|
||||
rule.Verbs[0],
|
||||
resourceName,
|
||||
schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]},
|
||||
|
Loading…
Reference in New Issue
Block a user