mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
Add integration tests for secondary authz
This commit is contained in:
parent
60bc5660de
commit
4d30c43494
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]},
|
||||||
|
Loading…
Reference in New Issue
Block a user