Add integration tests for secondary authz

This commit is contained in:
Joe Betz 2023-03-06 12:08:53 -05:00
parent 60bc5660de
commit 4d30c43494
2 changed files with 204 additions and 11 deletions

View File

@ -27,8 +27,11 @@ import (
"k8s.io/apiextensions-apiserver/test/integration/fixtures" "k8s.io/apiextensions-apiserver/test/integration/fixtures"
genericfeatures "k8s.io/apiserver/pkg/features" genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/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/etcd"
"k8s.io/kubernetes/test/integration/framework" "k8s.io/kubernetes/test/integration/framework"
@ -41,6 +44,7 @@ import (
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" 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 { func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1alpha1.ValidatingAdmissionPolicy) *admissionregistrationv1alpha1.ValidatingAdmissionPolicy {
policy = policy.DeepCopy() policy = policy.DeepCopy()
policy.Spec.MatchConstraints.ResourceRules = append(policy.Spec.MatchConstraints.ResourceRules, admissionregistrationv1alpha1.NamedRuleWithOperations{ 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 // no reason was given, no error was passed - early exit
return return
} }
reason := err.(apierrors.APIStatus).Status().Reason switch e := err.(type) {
if reason != expectedReason { case apierrors.APIStatus:
t.Logf("actual error reason: %v", reason) reason := e.Status().Reason
t.Logf("expected failure reason: %v", expectedReason) if reason != expectedReason {
t.Error("unexpected error reason") 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)
} }
} }

View File

@ -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) { 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() t.Helper()
cr, err := adminClient.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{ 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)}, ObjectMeta: metav1.ObjectMeta{GenerateName: strings.Replace(t.Name(), "/", "--", -1)},
Subjects: []rbacv1.Subject{ Subjects: []rbacv1.Subject{
{ {
Kind: rbacv1.UserKind, Kind: accountKind,
APIGroup: rbacv1.GroupName, // APIGroup defaults to the appropriate value for both users and service accounts
Name: username, Name: name,
Namespace: "", Namespace: namespace,
}, },
}, },
RoleRef: rbacv1.RoleRef{ RoleRef: rbacv1.RoleRef{
@ -107,12 +115,17 @@ func GrantUserAuthorization(t *testing.T, ctx context.Context, adminClient clien
resourceName = rule.ResourceNames[0] resourceName = rule.ResourceNames[0]
} }
subjectName := name
if accountKind == rbacv1.ServiceAccountKind {
subjectName = "system:serviceaccount:" + namespace + ":" + name
}
WaitForNamedAuthorizationUpdate( WaitForNamedAuthorizationUpdate(
t, t,
ctx, ctx,
adminClient.AuthorizationV1(), adminClient.AuthorizationV1(),
username, subjectName,
"", namespace,
rule.Verbs[0], rule.Verbs[0],
resourceName, resourceName,
schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]}, schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]},