mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-12-07 09:43:15 +00:00
Cache authz decisions within validating policy admission.
This avoids the surprise of identical authorization checks within a policy evaluating to different decisions during the same admission pass, and reduces the overhead of repeatedly referencing the same authorization check.
This commit is contained in:
@@ -18,12 +18,16 @@ package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
@@ -36,12 +40,15 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
|
||||
"k8s.io/kubernetes/test/integration/authutil"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
"k8s.io/kubernetes/test/utils"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -51,13 +58,11 @@ import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
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"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
)
|
||||
|
||||
// Test_ValidateNamespace_NoParams tests a ValidatingAdmissionPolicy that validates creation of a Namespace with no params.
|
||||
@@ -2451,15 +2456,15 @@ func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1alpha1.
|
||||
return policy
|
||||
}
|
||||
|
||||
func createAndWaitReady(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error {
|
||||
func createAndWaitReady(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error {
|
||||
return createAndWaitReadyNamespaced(t, client, binding, matchLabels, "default")
|
||||
}
|
||||
|
||||
func createAndWaitReadyNamespaced(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error {
|
||||
func createAndWaitReadyNamespaced(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error {
|
||||
return createAndWaitReadyNamespacedWithWarnHandler(t, client, binding, matchLabels, ns, newWarningHandler())
|
||||
}
|
||||
|
||||
func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string, handler *warningHandler) error {
|
||||
func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string, handler *warningHandler) error {
|
||||
marker := &v1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "test-marker", Namespace: ns, Labels: matchLabels}}
|
||||
defer func() {
|
||||
err := client.CoreV1().Endpoints(ns).Delete(context.TODO(), marker.Name, metav1.DeleteOptions{})
|
||||
@@ -3022,3 +3027,167 @@ func toHasLengthOf(n int) func(warnings []admissionregistrationv1alpha1.Expressi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationDecisionCaching(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
validations []admissionregistrationv1alpha1.Validation
|
||||
}{
|
||||
{
|
||||
name: "hit",
|
||||
validations: []admissionregistrationv1alpha1.Validation{
|
||||
{
|
||||
Expression: "authorizer.requestResource.check('test').reason() == authorizer.requestResource.check('test').reason()",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "miss",
|
||||
validations: []admissionregistrationv1alpha1.Validation{
|
||||
{
|
||||
Expression: "authorizer.requestResource.subresource('a').check('test').reason() == '1'",
|
||||
},
|
||||
{
|
||||
Expression: "authorizer.requestResource.subresource('b').check('test').reason() == '2'",
|
||||
},
|
||||
{
|
||||
Expression: "authorizer.requestResource.subresource('c').check('test').reason() == '3'",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
|
||||
var nChecks int
|
||||
webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var review authorizationv1.SubjectAccessReview
|
||||
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
review.Status.Allowed = true
|
||||
if review.Spec.ResourceAttributes.Verb == "test" {
|
||||
nChecks++
|
||||
review.Status.Reason = fmt.Sprintf("%d", nChecks)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(review); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer webhook.Close()
|
||||
|
||||
kcfd, err := os.CreateTemp("", "kubeconfig-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
func() {
|
||||
defer kcfd.Close()
|
||||
tmpl, err := template.New("kubeconfig").Parse(`
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- name: test-authz-service
|
||||
cluster:
|
||||
server: {{ .Server }}
|
||||
users:
|
||||
- name: test-api-server
|
||||
current-context: webhook
|
||||
contexts:
|
||||
- context:
|
||||
cluster: test-authz-service
|
||||
user: test-api-server
|
||||
name: webhook
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tmpl.Execute(kcfd, struct {
|
||||
Server string
|
||||
}{
|
||||
Server: webhook.URL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
client, config, teardown := framework.StartTestServer(ctx, t, framework.TestServerSetup{
|
||||
ModifyServerRunOptions: func(options *options.ServerRunOptions) {
|
||||
options.Admission.GenericAdmission.EnablePlugins = append(options.Admission.GenericAdmission.EnablePlugins, "ValidatingAdmissionPolicy")
|
||||
options.APIEnablement.RuntimeConfig.Set("api/all=true")
|
||||
|
||||
options.Authorization.Modes = []string{authzmodes.ModeWebhook}
|
||||
options.Authorization.WebhookConfigFile = kcfd.Name()
|
||||
options.Authorization.WebhookVersion = "v1"
|
||||
// Bypass webhook cache to observe the policy plugin's cache behavior.
|
||||
options.Authorization.WebhookCacheAuthorizedTTL = 0
|
||||
options.Authorization.WebhookCacheUnauthorizedTTL = 0
|
||||
},
|
||||
})
|
||||
defer teardown()
|
||||
|
||||
policy := &admissionregistrationv1alpha1.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-authorization-decision-caching-policy",
|
||||
},
|
||||
Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicySpec{
|
||||
MatchConstraints: &admissionregistrationv1alpha1.MatchResources{
|
||||
ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
ResourceNames: []string{"test-authorization-decision-caching-namespace"},
|
||||
RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1.OperationType{
|
||||
admissionregistrationv1.Create,
|
||||
},
|
||||
Rule: admissionregistrationv1.Rule{
|
||||
APIGroups: []string{""},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"namespaces"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Validations: tc.validations,
|
||||
},
|
||||
}
|
||||
|
||||
policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, withWaitReadyConstraintAndExpression(policy), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := createAndWaitReady(t, client, makeBinding(policy.Name+"-binding", policy.Name, ""), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config = rest.CopyConfig(config)
|
||||
config.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: "alice",
|
||||
UID: "1234",
|
||||
}
|
||||
client, err = clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := client.CoreV1().Namespaces().Create(
|
||||
ctx,
|
||||
&v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-authorization-decision-caching-namespace",
|
||||
},
|
||||
},
|
||||
metav1.CreateOptions{},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user