Merge pull request #88246 from munnerz/csr-signername-controllers
Update CSR controllers & kubelet to respect signerName field
This commit is contained in:
@@ -41,6 +41,7 @@ filegroup(
|
||||
"//test/integration/auth:all-srcs",
|
||||
"//test/integration/benchmark/extractlog:all-srcs",
|
||||
"//test/integration/benchmark/jsonify:all-srcs",
|
||||
"//test/integration/certificates:all-srcs",
|
||||
"//test/integration/client:all-srcs",
|
||||
"//test/integration/configmap:all-srcs",
|
||||
"//test/integration/cronjob:all-srcs",
|
||||
|
||||
47
test/integration/certificates/BUILD
Normal file
47
test/integration/certificates/BUILD
Normal file
@@ -0,0 +1,47 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_test")
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_approval_test.go",
|
||||
"admission_sign_test.go",
|
||||
"admission_subjectrestriction_test.go",
|
||||
"admission_test.go",
|
||||
"controller_approval_test.go",
|
||||
"defaulting_test.go",
|
||||
"field_selector_test.go",
|
||||
"main_test.go",
|
||||
],
|
||||
tags = ["integration"],
|
||||
deps = [
|
||||
"//cmd/kube-apiserver/app/testing:go_default_library",
|
||||
"//pkg/controller/certificates:go_default_library",
|
||||
"//pkg/controller/certificates/approver:go_default_library",
|
||||
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//test/integration/framework:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
147
test/integration/certificates/admission_approval_test.go
Normal file
147
test/integration/certificates/admission_approval_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to approve CSRs for the named signer
|
||||
func TestCSRSignerNameApprovalPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to approve requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to approve CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToApproveFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to approve.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to update the Approved condition.
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certv1beta1.CertificateApproved,
|
||||
Reason: "AutoApproved",
|
||||
Message: "Approved during integration test",
|
||||
})
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(csr)
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToApproveFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildApprovalClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("unable to create test fixture RBAC rules: %v", err)
|
||||
}
|
||||
approveRule := cr.Rules[0]
|
||||
updateRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", approveRule.Verbs[0], approveRule.ResourceNames[0], schema.GroupResource{Group: approveRule.APIGroups[0], Resource: approveRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", updateRule.Verbs[0], "", schema.GroupResource{Group: updateRule.APIGroups[0], Resource: updateRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildApprovalClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"approve"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/approval"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildClusterRoleBindingForUser(name, username, clusterRoleName string) *rbacv1.ClusterRoleBinding {
|
||||
return &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: username,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.SchemeGroupVersion.Group,
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
}
|
||||
124
test/integration/certificates/admission_sign_test.go
Normal file
124
test/integration/certificates/admission_sign_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CSR approval admission plugin correctly enforces that a
|
||||
// user has permission to sign CSRs for the named signer
|
||||
func TestCSRSignerNameSigningPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowedSignerName string
|
||||
signerName string
|
||||
error string
|
||||
}{
|
||||
"should admit when a user has permission for the exact signerName": {
|
||||
allowedSignerName: "example.com/something",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should admit when a user has permission for the wildcard-suffixed signerName": {
|
||||
allowedSignerName: "example.com/*",
|
||||
signerName: "example.com/something",
|
||||
},
|
||||
"should deny if a user does not have permission for the given signerName": {
|
||||
allowedSignerName: "example.com/not-something",
|
||||
signerName: "example.com/something",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: user not permitted to sign requests with signerName "example.com/something"`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{"--authorization-mode=RBAC"}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Grant 'test-user' permission to sign CertificateSigningRequests with the specified signerName.
|
||||
const username = "test-user"
|
||||
grantUserPermissionToSignFor(t, client, username, test.allowedSignerName)
|
||||
// Create a CSR to attempt to sign.
|
||||
csr := createTestingCSR(t, client.CertificatesV1beta1().CertificateSigningRequests(), "csr", test.signerName, "")
|
||||
|
||||
// Create a second client, impersonating the 'test-user' for us to test with.
|
||||
testuserConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
testuserConfig.Impersonate = restclient.ImpersonationConfig{UserName: username}
|
||||
testuserClient := clientset.NewForConfigOrDie(testuserConfig)
|
||||
|
||||
// Attempt to 'sign' the certificate.
|
||||
csr.Status.Certificate = []byte("dummy data")
|
||||
_, err := testuserClient.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserPermissionToSignFor(t *testing.T, client clientset.Interface, username string, signerNames ...string) {
|
||||
resourceName := "signername-" + username
|
||||
cr := buildSigningClusterRoleForSigners(resourceName, signerNames...)
|
||||
crb := buildClusterRoleBindingForUser(resourceName, username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
signRule := cr.Rules[0]
|
||||
statusRule := cr.Rules[1]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", signRule.Verbs[0], signRule.ResourceNames[0], schema.GroupResource{Group: signRule.APIGroups[0], Resource: signRule.Resources[0]}, true)
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", statusRule.Verbs[0], "", schema.GroupResource{Group: statusRule.APIGroups[0], Resource: statusRule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildSigningClusterRoleForSigners(name string, signerNames ...string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// must have permission to 'approve' the 'certificatesigners' named
|
||||
// 'signerName' to approve CSRs with the given signerName.
|
||||
{
|
||||
Verbs: []string{"sign"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"signers"},
|
||||
ResourceNames: signerNames,
|
||||
},
|
||||
{
|
||||
Verbs: []string{"update"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{"certificatesigningrequests/status"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the CertificateSubjectRestriction admission controller works as expected.
|
||||
func TestCertificateSubjectRestrictionPlugin(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signerName string
|
||||
group string
|
||||
error string
|
||||
}{
|
||||
"should reject a request if signerName is kube-apiserver-client and group is system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:masters",
|
||||
error: `certificatesigningrequests.certificates.k8s.io "csr" is forbidden: use of kubernetes.io/kube-apiserver-client signer with system:masters group is not allowed`,
|
||||
},
|
||||
"should admit a request if signerName is NOT kube-apiserver-client and org is system:masters": {
|
||||
signerName: certv1beta1.LegacyUnknownSignerName,
|
||||
group: "system:masters",
|
||||
},
|
||||
"should admit a request if signerName is kube-apiserver-client and group is NOT system:masters": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
group: "system:notmasters",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{""}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
|
||||
// Attempt to create the CSR resource.
|
||||
csr := buildTestingCSR("csr", test.signerName, test.group)
|
||||
_, err := client.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil && test.error != err.Error() {
|
||||
t.Errorf("expected error %q but got: %v", test.error, err)
|
||||
}
|
||||
if err == nil && test.error != "" {
|
||||
t.Errorf("expected to get an error %q but got none", test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
59
test/integration/certificates/admission_test.go
Normal file
59
test/integration/certificates/admission_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
v1authorization "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
)
|
||||
|
||||
// waitForNamedAuthorizationUpdate checks if the given user can perform the named verb and action on the named resource.
|
||||
// Copied from k8s.io/kubernetes/test/e2e/framework/auth.
|
||||
func waitForNamedAuthorizationUpdate(t *testing.T, c v1authorization.SubjectAccessReviewsGetter, user, namespace, verb, resourceName string, resource schema.GroupResource, allowed bool) {
|
||||
review := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Group: resource.Group,
|
||||
Verb: verb,
|
||||
Resource: resource.Resource,
|
||||
Namespace: namespace,
|
||||
Name: resourceName,
|
||||
},
|
||||
User: user,
|
||||
},
|
||||
}
|
||||
|
||||
if err := wait.Poll(time.Millisecond*100, time.Second*5, func() (bool, error) {
|
||||
response, err := c.SubjectAccessReviews().Create(context.TODO(), review, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if response.Status.Allowed != allowed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
220
test/integration/certificates/controller_approval_test.go
Normal file
220
test/integration/certificates/controller_approval_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
certv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/pkg/controller/certificates"
|
||||
"k8s.io/kubernetes/pkg/controller/certificates/approver"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Integration tests that verify the behaviour of the CSR auto-approving controller.
|
||||
func TestController_AutoApproval(t *testing.T) {
|
||||
validKubeAPIServerClientKubeletUsername := "system:node:abc"
|
||||
validKubeAPIServerClientKubeletCSR := pemWithTemplate(&x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: validKubeAPIServerClientKubeletUsername,
|
||||
Organization: []string{"system:nodes"},
|
||||
},
|
||||
})
|
||||
validKubeAPIServerClientKubeletUsages := []certv1beta1.KeyUsage{
|
||||
certv1beta1.UsageDigitalSignature,
|
||||
certv1beta1.UsageKeyEncipherment,
|
||||
certv1beta1.UsageClientAuth,
|
||||
}
|
||||
tests := map[string]struct {
|
||||
signerName string
|
||||
request []byte
|
||||
usages []certv1beta1.KeyUsage
|
||||
username string
|
||||
autoApproved bool
|
||||
grantNodeClient bool
|
||||
grantSelfNodeClient bool
|
||||
}{
|
||||
"should auto-approve CSR that has kube-apiserver-client-kubelet signerName and matches requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: validKubeAPIServerClientKubeletUsername,
|
||||
grantSelfNodeClient: true,
|
||||
autoApproved: true,
|
||||
},
|
||||
"should auto-approve CSR that has kube-apiserver-client-kubelet signerName and matches requirements despite missing username if nodeclient permissions are granted": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: "does-not-match-cn",
|
||||
grantNodeClient: true,
|
||||
autoApproved: true,
|
||||
},
|
||||
"should not auto-approve CSR that has kube-apiserver-client-kubelet signerName that does not match requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientKubeletSignerName,
|
||||
request: pemWithGroup("system:notnodes"),
|
||||
autoApproved: false,
|
||||
},
|
||||
"should not auto-approve CSR that has kube-apiserver-client signerName that DOES match kubelet CSR requirements": {
|
||||
signerName: certv1beta1.KubeAPIServerClientSignerName,
|
||||
request: validKubeAPIServerClientKubeletCSR,
|
||||
usages: validKubeAPIServerClientKubeletUsages,
|
||||
username: validKubeAPIServerClientKubeletUsername,
|
||||
autoApproved: false,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Run an apiserver with the default configuration options.
|
||||
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{""}, framework.SharedEtcd())
|
||||
defer s.TearDownFn()
|
||||
client := clientset.NewForConfigOrDie(s.ClientConfig)
|
||||
informers := informers.NewSharedInformerFactory(clientset.NewForConfigOrDie(restclient.AddUserAgent(s.ClientConfig, "certificatesigningrequest-informers")), time.Second)
|
||||
|
||||
// Register the controller
|
||||
c := approver.NewCSRApprovingController(client, informers.Certificates().V1beta1().CertificateSigningRequests())
|
||||
// Start the controller & informers
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
informers.Start(stopCh)
|
||||
go c.Run(1, stopCh)
|
||||
|
||||
// Configure appropriate permissions
|
||||
if test.grantNodeClient {
|
||||
grantUserNodeClientPermissions(t, client, test.username, false)
|
||||
}
|
||||
if test.grantSelfNodeClient {
|
||||
grantUserNodeClientPermissions(t, client, test.username, true)
|
||||
}
|
||||
|
||||
// Use a client that impersonates the test case 'username' to ensure the `spec.username`
|
||||
// field on the CSR is set correctly.
|
||||
impersonationConfig := restclient.CopyConfig(s.ClientConfig)
|
||||
impersonationConfig.Impersonate.UserName = test.username
|
||||
impersonationClient, err := clientset.NewForConfig(impersonationConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Error in create clientset: %v", err)
|
||||
}
|
||||
csr := &certv1beta1.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "csr",
|
||||
},
|
||||
Spec: certv1beta1.CertificateSigningRequestSpec{
|
||||
Request: test.request,
|
||||
Usages: test.usages,
|
||||
SignerName: &test.signerName,
|
||||
},
|
||||
}
|
||||
_, err = impersonationClient.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create testing CSR: %v", err)
|
||||
}
|
||||
|
||||
if test.autoApproved {
|
||||
if err := waitForCertificateRequestApproved(client, csr.Name); err != nil {
|
||||
t.Errorf("failed to wait for CSR to be auto-approved: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := ensureCertificateRequestNotApproved(client, csr.Name); err != nil {
|
||||
t.Errorf("failed to ensure that CSR was not auto-approved: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
interval = 100 * time.Millisecond
|
||||
timeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func waitForCertificateRequestApproved(client kubernetes.Interface, name string) error {
|
||||
if err := wait.Poll(interval, timeout, func() (bool, error) {
|
||||
csr, err := client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if certificates.IsCertificateRequestApproved(csr) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureCertificateRequestNotApproved(client kubernetes.Interface, name string) error {
|
||||
// If waiting for the CSR to be approved times out, we class this as 'not auto approved'.
|
||||
// There is currently no way to explicitly check if the CSR has been rejected for auto-approval.
|
||||
err := waitForCertificateRequestApproved(client, name)
|
||||
switch {
|
||||
case err == wait.ErrWaitTimeout:
|
||||
return nil
|
||||
case err == nil:
|
||||
return fmt.Errorf("CertificateSigningRequest was auto-approved")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func grantUserNodeClientPermissions(t *testing.T, client clientset.Interface, username string, selfNodeClient bool) {
|
||||
resourceType := "certificatesigningrequests/nodeclient"
|
||||
if selfNodeClient {
|
||||
resourceType = "certificatesigningrequests/selfnodeclient"
|
||||
}
|
||||
cr := buildNodeClientRoleForUser("role", resourceType)
|
||||
crb := buildClusterRoleBindingForUser("rolebinding", username, cr.Name)
|
||||
if _, err := client.RbacV1().ClusterRoles().Create(context.TODO(), cr, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
if _, err := client.RbacV1().ClusterRoleBindings().Create(context.TODO(), crb, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("failed to create test fixtures: %v", err)
|
||||
}
|
||||
rule := cr.Rules[0]
|
||||
waitForNamedAuthorizationUpdate(t, client.AuthorizationV1(), username, "", rule.Verbs[0], "", schema.GroupResource{Group: rule.APIGroups[0], Resource: rule.Resources[0]}, true)
|
||||
}
|
||||
|
||||
func buildNodeClientRoleForUser(name string, resourceType string) *rbacv1.ClusterRole {
|
||||
return &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"create"},
|
||||
APIGroups: []string{certv1beta1.SchemeGroupVersion.Group},
|
||||
Resources: []string{resourceType},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
79
test/integration/certificates/defaulting_test.go
Normal file
79
test/integration/certificates/defaulting_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the signerName field defaulting is wired up correctly.
|
||||
// An exhaustive set of test cases for all permutations of the possible
|
||||
// defaulting cases is written as a unit tests in the
|
||||
// `pkg/apis/certificates/...` directory.
|
||||
// This test cases exists to show that the defaulting function is wired up into
|
||||
// the apiserver correctly.
|
||||
func TestCSRSignerNameDefaulting(t *testing.T) {
|
||||
strPtr := func(s string) *string { return &s }
|
||||
tests := map[string]struct {
|
||||
csr capi.CertificateSigningRequestSpec
|
||||
expectedSignerName string
|
||||
}{
|
||||
"defaults to legacy-unknown if not recognised": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
},
|
||||
expectedSignerName: capi.LegacyUnknownSignerName,
|
||||
},
|
||||
"does not default signerName if an explicit value is provided": {
|
||||
csr: capi.CertificateSigningRequestSpec{
|
||||
Request: pemWithGroup(""),
|
||||
Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature},
|
||||
SignerName: strPtr("example.com/my-custom-signer"),
|
||||
},
|
||||
expectedSignerName: "example.com/my-custom-signer",
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, s, closeFn := framework.RunAMaster(nil)
|
||||
defer closeFn()
|
||||
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
csr := &capi.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testcsr"},
|
||||
Spec: test.csr,
|
||||
}
|
||||
csr, err := csrClient.Create(context.TODO(), csr, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create CSR resource: %v", err)
|
||||
}
|
||||
if *csr.Spec.SignerName != test.expectedSignerName {
|
||||
t.Errorf("expected CSR signerName to be %q but it was %q", test.expectedSignerName, *csr.Spec.SignerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
test/integration/certificates/field_selector_test.go
Normal file
127
test/integration/certificates/field_selector_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2020 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 certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
capi "k8s.io/api/certificates/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
certclientset "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
// Verifies that the 'spec.signerName' field can be correctly used as a field selector on LIST requests
|
||||
func TestCSRSignerNameFieldSelector(t *testing.T) {
|
||||
_, s, closeFn := framework.RunAMaster(nil)
|
||||
defer closeFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||
csrClient := client.CertificatesV1beta1().CertificateSigningRequests()
|
||||
csr1 := createTestingCSR(t, csrClient, "csr-1", "example.com/signer-name-1", "")
|
||||
csr2 := createTestingCSR(t, csrClient, "csr-2", "example.com/signer-name-2", "")
|
||||
// csr3 has the same signerName as csr2 so we can ensure multiple items are returned when running a filtered
|
||||
// LIST call.
|
||||
csr3 := createTestingCSR(t, csrClient, "csr-3", "example.com/signer-name-2", "")
|
||||
|
||||
signerOneList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-1"})
|
||||
if err != nil {
|
||||
t.Errorf("unable to list CSRs with spec.signerName=example.com/signer-name-1")
|
||||
return
|
||||
}
|
||||
if len(signerOneList.Items) != 1 {
|
||||
t.Errorf("expected one CSR to be returned but got %d", len(signerOneList.Items))
|
||||
} else if signerOneList.Items[0].Name != csr1.Name {
|
||||
t.Errorf("expected CSR named 'csr-1' to be returned but got %q", signerOneList.Items[0].Name)
|
||||
}
|
||||
|
||||
signerTwoList, err := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{FieldSelector: "spec.signerName=example.com/signer-name-2"})
|
||||
if err != nil {
|
||||
t.Errorf("unable to list CSRs with spec.signerName=example.com/signer-name-2")
|
||||
return
|
||||
}
|
||||
if len(signerTwoList.Items) != 2 {
|
||||
t.Errorf("expected one CSR to be returned but got %d", len(signerTwoList.Items))
|
||||
} else if signerTwoList.Items[0].Name != csr2.Name {
|
||||
t.Errorf("expected CSR named 'csr-2' to be returned but got %q", signerTwoList.Items[0].Name)
|
||||
} else if signerTwoList.Items[1].Name != csr3.Name {
|
||||
t.Errorf("expected CSR named 'csr-3' to be returned but got %q", signerTwoList.Items[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestingCSR(t *testing.T, certClient certclientset.CertificateSigningRequestInterface, name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName, groupName), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create testing CSR: %v", err)
|
||||
}
|
||||
return csr
|
||||
}
|
||||
|
||||
func buildTestingCSR(name, signerName, groupName string) *capi.CertificateSigningRequest {
|
||||
return &capi.CertificateSigningRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: capi.CertificateSigningRequestSpec{
|
||||
SignerName: &signerName,
|
||||
Request: pemWithGroup(groupName),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pemWithGroup(group string) []byte {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{group},
|
||||
},
|
||||
}
|
||||
return pemWithTemplate(template)
|
||||
}
|
||||
|
||||
func pemWithTemplate(template *x509.CertificateRequest) []byte {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
csrPemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
}
|
||||
|
||||
p := pem.EncodeToMemory(csrPemBlock)
|
||||
if p == nil {
|
||||
panic("invalid pem block")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
27
test/integration/certificates/main_test.go
Normal file
27
test/integration/certificates/main_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2017 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 certificates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
||||
Reference in New Issue
Block a user