Merge pull request #88246 from munnerz/csr-signername-controllers

Update CSR controllers & kubelet to respect signerName field
This commit is contained in:
Kubernetes Prow Robot
2020-02-28 23:38:39 -08:00
committed by GitHub
69 changed files with 3753 additions and 220 deletions

View File

@@ -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",

View 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"],
)

View 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,
},
}
}

View 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"},
},
},
}
}

View File

@@ -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)
}
})
}
}

View 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)
}
}

View 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},
},
},
}
}

View 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)
}
})
}
}

View 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
}

View 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)
}