diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 82d07dfdc3a..a6f71317e4c 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -4605,6 +4605,10 @@ "format": "byte", "type": "string" }, + "signerName": { + "description": "Requested signer for the request. It is a qualified name in the form: `scope-hostname.io/name`. If empty, it will be defaulted:\n 1. If it's a kubelet client certificate, it is assigned\n \"kubernetes.io/kube-apiserver-client-kubelet\".\n 2. If it's a kubelet serving certificate, it is assigned\n \"kubernetes.io/kubelet-serving\".\n 3. Otherwise, it is assigned \"kubernetes.io/legacy-unknown\".\nDistribution of trust for signers happens out of band. You can select on this field using `spec.signerName`.", + "type": "string" + }, "uid": { "description": "UID information about the requesting user. See user.Info interface for details.", "type": "string" diff --git a/cmd/kubelet/app/server_bootstrap_test.go b/cmd/kubelet/app/server_bootstrap_test.go index 7da288ab1b5..9fdacf8e34f 100644 --- a/cmd/kubelet/app/server_bootstrap_test.go +++ b/cmd/kubelet/app/server_bootstrap_test.go @@ -300,7 +300,7 @@ func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) { PrivateKey: s.serverPrivateKey, Backdate: s.backdate, } - cr, err := capihelper.ParseCSR(csr) + cr, err := capihelper.ParseCSR(csr.Spec.Request) if err != nil { t.Fatal(err) } diff --git a/pkg/apis/certificates/fuzzer/fuzzer.go b/pkg/apis/certificates/fuzzer/fuzzer.go index ccfca013bed..962eb2898b7 100644 --- a/pkg/apis/certificates/fuzzer/fuzzer.go +++ b/pkg/apis/certificates/fuzzer/fuzzer.go @@ -29,6 +29,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { func(obj *certificates.CertificateSigningRequestSpec, c fuzz.Continue) { c.FuzzNoCustom(obj) // fuzz self without calling this function again obj.Usages = []certificates.KeyUsage{certificates.UsageKeyEncipherment} + obj.SignerName = "example.com/custom-sample-signer" }, } } diff --git a/pkg/apis/certificates/types.go b/pkg/apis/certificates/types.go index f6f4dceeef1..b3a07f8d7cd 100644 --- a/pkg/apis/certificates/types.go +++ b/pkg/apis/certificates/types.go @@ -42,6 +42,12 @@ type CertificateSigningRequestSpec struct { // Base64-encoded PKCS#10 CSR data Request []byte + // Requested signer for the request. It is a qualified name in the form: + // `scope-hostname.io/name`. + // Distribution of trust for signers happens out of band. + // You can select on this field using `spec.signerName`. + SignerName string + // usages specifies a set of usage contexts the key will be // valid for. // See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 diff --git a/pkg/apis/certificates/v1beta1/BUILD b/pkg/apis/certificates/v1beta1/BUILD index e28449dd4c4..057b22b2ade 100644 --- a/pkg/apis/certificates/v1beta1/BUILD +++ b/pkg/apis/certificates/v1beta1/BUILD @@ -3,11 +3,13 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( name = "go_default_library", srcs = [ + "conversion.go", "defaults.go", "doc.go", "helpers.go", @@ -19,9 +21,11 @@ go_library( deps = [ "//pkg/apis/certificates:go_default_library", "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", ], ) @@ -37,3 +41,10 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["defaults_test.go"], + embed = [":go_default_library"], + deps = ["//staging/src/k8s.io/api/certificates/v1beta1:go_default_library"], +) diff --git a/pkg/apis/certificates/v1beta1/conversion.go b/pkg/apis/certificates/v1beta1/conversion.go new file mode 100644 index 00000000000..d4d8af98739 --- /dev/null +++ b/pkg/apis/certificates/v1beta1/conversion.go @@ -0,0 +1,38 @@ +/* +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 v1beta1 + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" +) + +func addConversionFuncs(scheme *runtime.Scheme) error { + // Add field conversion funcs. + return scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("CertificateSigningRequest"), + func(label, value string) (string, string, error) { + switch label { + case "metadata.name", + "spec.signerName": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }, + ) +} diff --git a/pkg/apis/certificates/v1beta1/defaults.go b/pkg/apis/certificates/v1beta1/defaults.go index a30a8b3ed25..ec11d3ab111 100644 --- a/pkg/apis/certificates/v1beta1/defaults.go +++ b/pkg/apis/certificates/v1beta1/defaults.go @@ -17,15 +17,113 @@ limitations under the License. package v1beta1 import ( + "crypto/x509" + "reflect" + "strings" + certificatesv1beta1 "k8s.io/api/certificates/v1beta1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { return RegisterDefaults(scheme) } + func SetDefaults_CertificateSigningRequestSpec(obj *certificatesv1beta1.CertificateSigningRequestSpec) { if obj.Usages == nil { obj.Usages = []certificatesv1beta1.KeyUsage{certificatesv1beta1.UsageDigitalSignature, certificatesv1beta1.UsageKeyEncipherment} } + + if obj.SignerName == nil { + signerName := DefaultSignerNameFromSpec(obj) + obj.SignerName = &signerName + } +} + +// DefaultSignerNameFromSpec will determine the signerName that should be set +// by attempting to inspect the 'request' content and the spec options. +func DefaultSignerNameFromSpec(obj *certificatesv1beta1.CertificateSigningRequestSpec) string { + csr, err := ParseCSR(obj.Request) + switch { + case err != nil: + // Set the signerName to 'legacy-unknown' as the CSR could not be + // recognised. + return certificatesv1beta1.LegacyUnknownSignerName + case IsKubeletClientCSR(csr, obj.Usages): + return certificatesv1beta1.KubeAPIServerClientKubeletSignerName + case IsKubeletServingCSR(csr, obj.Usages): + return certificatesv1beta1.KubeletServingSignerName + default: + return certificatesv1beta1.LegacyUnknownSignerName + } +} + +func IsKubeletServingCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool { + if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) { + return false + } + + // at least one of dnsNames or ipAddresses must be specified + if len(req.DNSNames) == 0 && len(req.IPAddresses) == 0 { + return false + } + + if len(req.EmailAddresses) > 0 || len(req.URIs) > 0 { + return false + } + + requiredUsages := []certificatesv1beta1.KeyUsage{ + certificatesv1beta1.UsageDigitalSignature, + certificatesv1beta1.UsageKeyEncipherment, + certificatesv1beta1.UsageServerAuth, + } + if !equalUnsorted(requiredUsages, usages) { + return false + } + + if !strings.HasPrefix(req.Subject.CommonName, "system:node:") { + return false + } + + return true +} + +func IsKubeletClientCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool { + if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) { + return false + } + + if len(req.DNSNames) > 0 || len(req.EmailAddresses) > 0 || len(req.IPAddresses) > 0 || len(req.URIs) > 0 { + return false + } + + if !strings.HasPrefix(req.Subject.CommonName, "system:node:") { + return false + } + + requiredUsages := []certificatesv1beta1.KeyUsage{ + certificatesv1beta1.UsageDigitalSignature, + certificatesv1beta1.UsageKeyEncipherment, + certificatesv1beta1.UsageClientAuth, + } + if !equalUnsorted(requiredUsages, usages) { + return false + } + + return true +} + +// equalUnsorted compares two []string for equality of contents regardless of +// the order of the elements +func equalUnsorted(left, right []certificatesv1beta1.KeyUsage) bool { + l := sets.NewString() + for _, s := range left { + l.Insert(string(s)) + } + r := sets.NewString() + for _, s := range right { + r.Insert(string(s)) + } + return l.Equal(r) } diff --git a/pkg/apis/certificates/v1beta1/defaults_test.go b/pkg/apis/certificates/v1beta1/defaults_test.go new file mode 100644 index 00000000000..41062082a57 --- /dev/null +++ b/pkg/apis/certificates/v1beta1/defaults_test.go @@ -0,0 +1,353 @@ +/* +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 v1beta1 + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "net" + "net/url" + "reflect" + "testing" + + capi "k8s.io/api/certificates/v1beta1" +) + +var ( + kubeletClientUsages = []capi.KeyUsage{ + capi.UsageDigitalSignature, + capi.UsageKeyEncipherment, + capi.UsageClientAuth, + } + kubeletClientPEMOptions = pemOptions{ + cn: "system:node:nodename", + org: "system:nodes", + } + + kubeletServerUsages = []capi.KeyUsage{ + capi.UsageDigitalSignature, + capi.UsageKeyEncipherment, + capi.UsageServerAuth, + } + kubeletServerPEMOptions = pemOptions{ + cn: "system:node:requester-name", + org: "system:nodes", + dnsNames: []string{"node-server-name"}, + ipAddresses: []net.IP{{0, 0, 0, 0}}, + } +) + +func TestSetDefaults_CertificateSigningRequestSpec(t *testing.T) { + strPtr := func(s string) *string { return &s } + tests := map[string]struct { + csr capi.CertificateSigningRequestSpec + expectedSignerName string + expectedUsages []capi.KeyUsage + }{ + "defaults to legacy-unknown if request is not a CSR": { + csr: capi.CertificateSigningRequestSpec{ + Request: []byte("invalid data"), + Usages: kubeletServerUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default signerName if signerName is already set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions), + Usages: kubeletServerUsages, + SignerName: strPtr("example.com/not-kubelet-serving"), + }, + expectedSignerName: "example.com/not-kubelet-serving", + }, + "defaults usages if not set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions), + SignerName: strPtr("example.com/test"), + }, + expectedSignerName: "example.com/test", + expectedUsages: []capi.KeyUsage{capi.UsageDigitalSignature, capi.UsageKeyEncipherment}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // create a deepcopy to be sure we don't modify anything in-place + csrSpec := test.csr.DeepCopy() + SetDefaults_CertificateSigningRequestSpec(csrSpec) + if *csrSpec.SignerName != test.expectedSignerName { + t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName) + } + + // only check expectedUsages if it is non-nil + if test.expectedUsages != nil { + if !reflect.DeepEqual(test.expectedUsages, csrSpec.Usages) { + t.Errorf("expected usages to be defaulted to %v but it is %v", test.expectedUsages, csrSpec.Usages) + } + } + }) + } +} + +func TestSetDefaults_CertificateSigningRequestSpec_KubeletServing(t *testing.T) { + tests := map[string]struct { + csr capi.CertificateSigningRequestSpec + expectedSignerName string + }{ + "defaults for kubelet-serving": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions), + Usages: kubeletServerUsages, + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.KubeletServingSignerName, + }, + "does not default to kube-apiserver-client-kubelet if org is not 'system:nodes'": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{org: "not-system:nodes"}), + Usages: kubeletServerUsages, + Username: "system:node:not-requester-name", + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if CN does not have system:node: prefix": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{cn: "notprefixed"}), + Usages: kubeletServerUsages, + Username: "notprefixed", + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if it has an unexpected usage": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions), + Usages: append(kubeletServerUsages, capi.UsageClientAuth), + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if it is missing an expected usage": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions), + // Remove the first usage in 'kubeletServerUsages' + Usages: kubeletServerUsages[1:], + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if it does not specify any dnsNames or ipAddresses": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{ipAddresses: []net.IP{}, dnsNames: []string{}}), + Usages: kubeletServerUsages, + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if it specifies a URI SAN": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{uris: []string{"http://something"}}), + Usages: kubeletServerUsages, + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kubelet-serving if it specifies an emailAddress SAN": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletServerPEMOptions, pemOptions{emailAddresses: []string{"something"}}), + Usages: kubeletServerUsages, + Username: kubeletServerPEMOptions.cn, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // create a deepcopy to be sure we don't modify anything in-place + csrSpec := test.csr.DeepCopy() + SetDefaults_CertificateSigningRequestSpec(csrSpec) + if *csrSpec.SignerName != test.expectedSignerName { + t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName) + } + }) + } +} + +func TestSetDefaults_CertificateSigningRequestSpec_KubeletClient(t *testing.T) { + tests := map[string]struct { + csr capi.CertificateSigningRequestSpec + expectedSignerName string + }{ + "defaults for kube-apiserver-client-kubelet": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.KubeAPIServerClientKubeletSignerName, + }, + "does not default to kube-apiserver-client-kubelet if org is not 'system:nodes'": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{org: "not-system:nodes"}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if a dnsName is set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{dnsNames: []string{"something"}}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if an emailAddress is set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{emailAddresses: []string{"something"}}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if a uri SAN is set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{uris: []string{"http://something"}}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if an ipAddress is set": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{ipAddresses: []net.IP{{0, 0, 0, 0}}}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if CN does not have 'system:node:' prefix": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions, pemOptions{cn: "not-prefixed"}), + Usages: kubeletClientUsages, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if it has an unexpected usage": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions), + Usages: append(kubeletClientUsages, capi.UsageServerAuth), + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default to kube-apiserver-client-kubelet if it is missing an expected usage": { + csr: capi.CertificateSigningRequestSpec{ + Request: csrWithOpts(kubeletClientPEMOptions), + // Remove the first usage in 'kubeletClientUsages' + Usages: kubeletClientUsages[1:], + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // create a deepcopy to be sure we don't modify anything in-place + csrSpec := test.csr.DeepCopy() + SetDefaults_CertificateSigningRequestSpec(csrSpec) + if *csrSpec.SignerName != test.expectedSignerName { + t.Errorf("expected signerName to be defaulted to %q but it is %q", test.expectedSignerName, *csrSpec.SignerName) + } + }) + } +} + +type pemOptions struct { + cn string + org string + ipAddresses []net.IP + dnsNames []string + emailAddresses []string + uris []string +} + +// overlayPEMOptions overlays one set of pemOptions on top of another to allow +// for easily overriding a single field in the options +func overlayPEMOptions(opts ...pemOptions) pemOptions { + if len(opts) == 0 { + return pemOptions{} + } + base := opts[0] + for _, opt := range opts[1:] { + if opt.cn != "" { + base.cn = opt.cn + } + if opt.org != "" { + base.org = opt.org + } + if opt.ipAddresses != nil { + base.ipAddresses = opt.ipAddresses + } + if opt.dnsNames != nil { + base.dnsNames = opt.dnsNames + } + if opt.emailAddresses != nil { + base.emailAddresses = opt.emailAddresses + } + if opt.uris != nil { + base.uris = opt.uris + } + } + return base +} + +func csrWithOpts(base pemOptions, overlays ...pemOptions) []byte { + opts := overlayPEMOptions(append([]pemOptions{base}, overlays...)...) + uris := make([]*url.URL, len(opts.uris)) + for i, s := range opts.uris { + u, err := url.ParseRequestURI(s) + if err != nil { + panic(err) + } + uris[i] = u + } + template := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: opts.cn, + Organization: []string{opts.org}, + }, + IPAddresses: opts.ipAddresses, + DNSNames: opts.dnsNames, + EmailAddresses: opts.emailAddresses, + URIs: uris, + } + + _, 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 +} diff --git a/pkg/apis/certificates/v1beta1/helpers.go b/pkg/apis/certificates/v1beta1/helpers.go index 22bdb1fb5d9..28fa53b6ef9 100644 --- a/pkg/apis/certificates/v1beta1/helpers.go +++ b/pkg/apis/certificates/v1beta1/helpers.go @@ -20,14 +20,11 @@ import ( "crypto/x509" "encoding/pem" "errors" - - certificatesv1beta1 "k8s.io/api/certificates/v1beta1" ) -// ParseCSR extracts the CSR from the API object and decodes it. -func ParseCSR(obj *certificatesv1beta1.CertificateSigningRequest) (*x509.CertificateRequest, error) { +// ParseCSR decodes a PEM encoded CSR +func ParseCSR(pemBytes []byte) (*x509.CertificateRequest, error) { // extract PEM from request object - pemBytes := obj.Spec.Request block, _ := pem.Decode(pemBytes) if block == nil || block.Type != "CERTIFICATE REQUEST" { return nil, errors.New("PEM block type must be CERTIFICATE REQUEST") diff --git a/pkg/apis/certificates/v1beta1/register.go b/pkg/apis/certificates/v1beta1/register.go index dbb0e801660..6f5a33a78ab 100644 --- a/pkg/apis/certificates/v1beta1/register.go +++ b/pkg/apis/certificates/v1beta1/register.go @@ -46,5 +46,5 @@ func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. - localSchemeBuilder.Register(addDefaultingFuncs) + localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs) } diff --git a/pkg/apis/certificates/v1beta1/zz_generated.conversion.go b/pkg/apis/certificates/v1beta1/zz_generated.conversion.go index 0eaaf543441..e8c87716e59 100644 --- a/pkg/apis/certificates/v1beta1/zz_generated.conversion.go +++ b/pkg/apis/certificates/v1beta1/zz_generated.conversion.go @@ -24,6 +24,7 @@ import ( unsafe "unsafe" v1beta1 "k8s.io/api/certificates/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" certificates "k8s.io/kubernetes/pkg/apis/certificates" @@ -149,7 +150,17 @@ func Convert_certificates_CertificateSigningRequestCondition_To_v1beta1_Certific func autoConvert_v1beta1_CertificateSigningRequestList_To_certificates_CertificateSigningRequestList(in *v1beta1.CertificateSigningRequestList, out *certificates.CertificateSigningRequestList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]certificates.CertificateSigningRequest)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]certificates.CertificateSigningRequest, len(*in)) + for i := range *in { + if err := Convert_v1beta1_CertificateSigningRequest_To_certificates_CertificateSigningRequest(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -160,7 +171,17 @@ func Convert_v1beta1_CertificateSigningRequestList_To_certificates_CertificateSi func autoConvert_certificates_CertificateSigningRequestList_To_v1beta1_CertificateSigningRequestList(in *certificates.CertificateSigningRequestList, out *v1beta1.CertificateSigningRequestList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]v1beta1.CertificateSigningRequest)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]v1beta1.CertificateSigningRequest, len(*in)) + for i := range *in { + if err := Convert_certificates_CertificateSigningRequest_To_v1beta1_CertificateSigningRequest(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -171,6 +192,9 @@ func Convert_certificates_CertificateSigningRequestList_To_v1beta1_CertificateSi func autoConvert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSigningRequestSpec(in *v1beta1.CertificateSigningRequestSpec, out *certificates.CertificateSigningRequestSpec, s conversion.Scope) error { out.Request = *(*[]byte)(unsafe.Pointer(&in.Request)) + if err := v1.Convert_Pointer_string_To_string(&in.SignerName, &out.SignerName, s); err != nil { + return err + } out.Usages = *(*[]certificates.KeyUsage)(unsafe.Pointer(&in.Usages)) out.Username = in.Username out.UID = in.UID @@ -186,6 +210,9 @@ func Convert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSi func autoConvert_certificates_CertificateSigningRequestSpec_To_v1beta1_CertificateSigningRequestSpec(in *certificates.CertificateSigningRequestSpec, out *v1beta1.CertificateSigningRequestSpec, s conversion.Scope) error { out.Request = *(*[]byte)(unsafe.Pointer(&in.Request)) + if err := v1.Convert_string_To_Pointer_string(&in.SignerName, &out.SignerName, s); err != nil { + return err + } out.Usages = *(*[]v1beta1.KeyUsage)(unsafe.Pointer(&in.Usages)) out.Username = in.Username out.UID = in.UID diff --git a/pkg/apis/certificates/validation/BUILD b/pkg/apis/certificates/validation/BUILD index d09338ae0cc..93e1542f2b8 100644 --- a/pkg/apis/certificates/validation/BUILD +++ b/pkg/apis/certificates/validation/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -12,6 +13,7 @@ go_library( deps = [ "//pkg/apis/certificates:go_default_library", "//pkg/apis/core/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", ], ) @@ -28,3 +30,14 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certificates:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) diff --git a/pkg/apis/certificates/validation/validation.go b/pkg/apis/certificates/validation/validation.go index 3b61074bc6b..d24446355af 100644 --- a/pkg/apis/certificates/validation/validation.go +++ b/pkg/apis/certificates/validation/validation.go @@ -18,8 +18,11 @@ package validation import ( "fmt" + "strings" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/apis/certificates" apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" ) @@ -48,19 +51,95 @@ func ValidateCertificateRequestName(name string, prefix bool) []string { func ValidateCertificateSigningRequest(csr *certificates.CertificateSigningRequest) field.ErrorList { isNamespaced := false allErrs := apivalidation.ValidateObjectMeta(&csr.ObjectMeta, isNamespaced, ValidateCertificateRequestName, field.NewPath("metadata")) - err := validateCSR(csr) specPath := field.NewPath("spec") - + err := validateCSR(csr) if err != nil { allErrs = append(allErrs, field.Invalid(specPath.Child("request"), csr.Spec.Request, fmt.Sprintf("%v", err))) } if len(csr.Spec.Usages) == 0 { allErrs = append(allErrs, field.Required(specPath.Child("usages"), "usages must be provided")) } + allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) return allErrs } +// ensure signerName is of the form domain.com/something and up to 571 characters. +// This length and format is specified to accommodate signerNames like: +// /.. +// The max length of a FQDN is 253 characters (DNS1123Subdomain max length) +// The max length of a namespace name is 63 characters (DNS1123Label max length) +// The max length of a resource name is 253 characters (DNS1123Subdomain max length) +// We then add an additional 2 characters to account for the one '.' and one '/'. +func ValidateCertificateSigningRequestSignerName(fldPath *field.Path, signerName string) field.ErrorList { + var el field.ErrorList + if len(signerName) == 0 { + el = append(el, field.Required(fldPath, "signerName must be provided")) + return el + } + + segments := strings.Split(signerName, "/") + // validate that there is one '/' in the signerName. + // we do this after validating the domain segment to provide more info to the user. + if len(segments) != 2 { + el = append(el, field.Invalid(fldPath, signerName, "must be a fully qualified domain and path of the form 'example.com/signer-name'")) + // return early here as we should not continue attempting to validate a missing or malformed path segment + // (i.e. one containing multiple or zero `/`) + return el + } + + // validate that segments[0] is less than 253 characters altogether + maxDomainSegmentLength := utilvalidation.DNS1123SubdomainMaxLength + if len(segments[0]) > maxDomainSegmentLength { + el = append(el, field.TooLong(fldPath, segments[0], maxDomainSegmentLength)) + } + // validate that segments[0] consists of valid DNS1123 labels separated by '.' + domainLabels := strings.Split(segments[0], ".") + for _, lbl := range domainLabels { + // use IsDNS1123Label as we want to ensure the max length of any single label in the domain + // is 63 characters + if errs := utilvalidation.IsDNS1123Label(lbl); len(errs) > 0 { + for _, err := range errs { + el = append(el, field.Invalid(fldPath, segments[0], fmt.Sprintf("validating label %q: %s", lbl, err))) + } + // if we encounter any errors whilst parsing the domain segment, break from + // validation as any further error messages will be duplicates, and non-distinguishable + // from each other, confusing users. + break + } + } + + // validate that there is at least one '.' in segments[0] + if len(domainLabels) < 2 { + el = append(el, field.Invalid(fldPath, segments[0], "should be a domain with at least two segments separated by dots")) + } + + // validate that segments[1] consists of valid DNS1123 subdomains separated by '.'. + pathLabels := strings.Split(segments[1], ".") + for _, lbl := range pathLabels { + // use IsDNS1123Subdomain because it enforces a length restriction of 253 characters + // which is required in order to fit a full resource name into a single 'label' + if errs := utilvalidation.IsDNS1123Subdomain(lbl); len(errs) > 0 { + for _, err := range errs { + el = append(el, field.Invalid(fldPath, segments[1], fmt.Sprintf("validating label %q: %s", lbl, err))) + } + // if we encounter any errors whilst parsing the path segment, break from + // validation as any further error messages will be duplicates, and non-distinguishable + // from each other, confusing users. + break + } + } + + // ensure that segments[1] can accommodate a dns label + dns subdomain + '.' + maxPathSegmentLength := utilvalidation.DNS1123SubdomainMaxLength + utilvalidation.DNS1123LabelMaxLength + 1 + maxSignerNameLength := maxDomainSegmentLength + maxPathSegmentLength + 1 + if len(signerName) > maxSignerNameLength { + el = append(el, field.TooLong(fldPath, signerName, maxSignerNameLength)) + } + + return el +} + func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest) field.ErrorList { validationErrorList := ValidateCertificateSigningRequest(newCSR) metaUpdateErrorList := apivalidation.ValidateObjectMetaUpdate(&newCSR.ObjectMeta, &oldCSR.ObjectMeta, field.NewPath("metadata")) diff --git a/pkg/apis/certificates/validation/validation_test.go b/pkg/apis/certificates/validation/validation_test.go new file mode 100644 index 00000000000..12857c3fbd0 --- /dev/null +++ b/pkg/apis/certificates/validation/validation_test.go @@ -0,0 +1,308 @@ +/* +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 validation + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "reflect" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + capi "k8s.io/kubernetes/pkg/apis/certificates" +) + +var ( + validObjectMeta = metav1.ObjectMeta{Name: "testcsr"} + validSignerName = "example.com/valid-name" + validUsages = []capi.KeyUsage{capi.UsageKeyEncipherment} +) + +func TestValidateCertificateSigningRequest(t *testing.T) { + specPath := field.NewPath("spec") + // maxLengthSignerName is a signerName that is of maximum length, utilising + // the max length specifications defined in validation.go. + // It is of the form /. + maxLengthFQDN := fmt.Sprintf("%s.%s.%s.%s", repeatString("a", 63), repeatString("a", 63), repeatString("a", 63), repeatString("a", 61)) + maxLengthSignerName := fmt.Sprintf("%s/%s.%s", maxLengthFQDN, repeatString("a", 63), repeatString("a", 253)) + tests := map[string]struct { + csr capi.CertificateSigningRequest + errs field.ErrorList + }{ + "CSR with empty request data should fail": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + SignerName: validSignerName, + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("request"), []byte(nil), "PEM block type must be CERTIFICATE REQUEST"), + }, + }, + "CSR with invalid request data should fail": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + SignerName: validSignerName, + Request: []byte("invalid data"), + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("request"), []byte("invalid data"), "PEM block type must be CERTIFICATE REQUEST"), + }, + }, + "CSR with no usages should fail": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + SignerName: validSignerName, + Request: newCSRPEM(t), + }, + }, + errs: field.ErrorList{ + field.Required(specPath.Child("usages"), "usages must be provided"), + }, + }, + "CSR with no signerName set should fail": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + }, + }, + errs: field.ErrorList{ + field.Required(specPath.Child("signerName"), "signerName must be provided"), + }, + }, + "signerName contains no '/'": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "an-invalid-signer-name", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name", "must be a fully qualified domain and path of the form 'example.com/signer-name'"), + }, + }, + "signerName contains two '/'": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "an-invalid-signer-name.com/something/else", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name.com/something/else", "must be a fully qualified domain and path of the form 'example.com/signer-name'"), + }, + }, + "signerName domain component is not fully qualified": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example/some-signer-name", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "example", "should be a domain with at least two segments separated by dots"), + }, + }, + "signerName path component is empty": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "", `validating label "": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), + }, + }, + "signerName path component ends with a symbol": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/something-", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "something-", `validating label "something-": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), + }, + }, + "signerName path component is a symbol": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/-", + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), "-", `validating label "-": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), + }, + }, + "signerName path component contains no '.' but is valid": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/some-signer-name", + }, + }, + errs: field.ErrorList{}, + }, + "signerName with a total length greater than 571 characters should be rejected": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + // this string is longer than the max signerName limit (635 chars) + SignerName: maxLengthSignerName + ".toolong", + }, + }, + errs: field.ErrorList{ + field.TooLong(specPath.Child("signerName"), maxLengthSignerName+".toolong", len(maxLengthSignerName)), + }, + }, + "signerName with a fqdn greater than 253 characters should be rejected": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + // this string is longer than the max signerName limit (635 chars) + SignerName: fmt.Sprintf("%s.extra/valid-path", maxLengthFQDN), + }, + }, + errs: field.ErrorList{ + field.TooLong(specPath.Child("signerName"), fmt.Sprintf("%s.extra", maxLengthFQDN), len(maxLengthFQDN)), + }, + }, + "signerName can have a longer path if the domain component is less than the max length": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: fmt.Sprintf("abc.io/%s.%s", repeatString("a", 253), repeatString("a", 253)), + }, + }, + errs: field.ErrorList{}, + }, + "signerName with a domain label greater than 63 characters will fail": { + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: fmt.Sprintf("%s.example.io/valid-path", repeatString("a", 66)), + }, + }, + errs: field.ErrorList{ + field.Invalid(specPath.Child("signerName"), fmt.Sprintf("%s.example.io", repeatString("a", 66)), fmt.Sprintf(`validating label "%s": must be no more than 63 characters`, repeatString("a", 66))), + }, + }, + "signerName of max length in format /. is valid": { + // ensure signerName is of the form domain.com/something and up to 571 characters. + // This length and format is specified to accommodate signerNames like: + // /.. + // The max length of a FQDN is 253 characters (DNS1123Subdomain max length) + // The max length of a namespace name is 63 characters (DNS1123Label max length) + // The max length of a resource name is 253 characters (DNS1123Subdomain max length) + // We then add an additional 2 characters to account for the one '.' and one '/'. + csr: capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: maxLengthSignerName, + }, + }, + errs: field.ErrorList{}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + el := ValidateCertificateSigningRequest(&test.csr) + if !reflect.DeepEqual(el, test.errs) { + t.Errorf("returned and expected errors did not match - expected %v but got %v", test.errs.ToAggregate(), el.ToAggregate()) + } + }) + } +} + +func repeatString(s string, num int) string { + l := make([]string, num) + for i := 0; i < num; i++ { + l[i] = s + } + return strings.Join(l, "") +} + +func newCSRPEM(t *testing.T) []byte { + template := &x509.CertificateRequest{ + Subject: pkix.Name{ + Organization: []string{"testing-org"}, + }, + } + + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + t.Fatal(err) + } + + csrPemBlock := &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrDER, + } + + p := pem.EncodeToMemory(csrPemBlock) + if p == nil { + t.Fatal("invalid pem block") + } + + return p +} diff --git a/pkg/controller/certificates/approver/sarapprove.go b/pkg/controller/certificates/approver/sarapprove.go index 413e094bc22..3af1eee76ff 100644 --- a/pkg/controller/certificates/approver/sarapprove.go +++ b/pkg/controller/certificates/approver/sarapprove.go @@ -81,7 +81,7 @@ func (a *sarApprover) handle(csr *capi.CertificateSigningRequest) error { if approved, denied := certificates.GetCertApprovalCondition(&csr.Status); approved || denied { return nil } - x509cr, err := capihelper.ParseCSR(csr) + x509cr, err := capihelper.ParseCSR(csr.Spec.Request) if err != nil { return fmt.Errorf("unable to parse csr %q: %v", csr.Name, err) } diff --git a/pkg/controller/certificates/approver/sarapprove_test.go b/pkg/controller/certificates/approver/sarapprove_test.go index 786bb731adb..e20f26ba9b0 100644 --- a/pkg/controller/certificates/approver/sarapprove_test.go +++ b/pkg/controller/certificates/approver/sarapprove_test.go @@ -242,7 +242,7 @@ func testRecognizer(t *testing.T, cases []func(b *csrBuilder), recognizeFunc fun c(&b) t.Run(fmt.Sprintf("csr:%#v", b), func(t *testing.T) { csr := makeFancyTestCsr(b) - x509cr, err := k8s_certificates_v1beta1.ParseCSR(csr) + x509cr, err := k8s_certificates_v1beta1.ParseCSR(csr.Spec.Request) if err != nil { t.Errorf("unexpected err: %v", err) } diff --git a/pkg/controller/certificates/signer/signer.go b/pkg/controller/certificates/signer/signer.go index 27ccf0e553a..da9e8e35760 100644 --- a/pkg/controller/certificates/signer/signer.go +++ b/pkg/controller/certificates/signer/signer.go @@ -104,7 +104,7 @@ func (s *signer) handle(csr *capi.CertificateSigningRequest) error { } func (s *signer) sign(csr *capi.CertificateSigningRequest) (*capi.CertificateSigningRequest, error) { - x509cr, err := capihelper.ParseCSR(csr) + x509cr, err := capihelper.ParseCSR(csr.Spec.Request) if err != nil { return nil, fmt.Errorf("unable to parse csr %q: %v", csr.Name, err) } diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 67974c6da12..b01714cdd1f 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -397,6 +397,7 @@ func AddHandlers(h printers.PrintHandler) { certificateSigningRequestColumnDefinitions := []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + {Name: "SignerName", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestSpec{}.SwaggerDoc()["signerName"]}, {Name: "Requestor", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestSpec{}.SwaggerDoc()["request"]}, {Name: "Condition", Type: "string", Description: certificatesv1beta1.CertificateSigningRequestStatus{}.SwaggerDoc()["conditions"]}, } @@ -1716,7 +1717,11 @@ func printCertificateSigningRequest(obj *certificates.CertificateSigningRequest, if err != nil { return nil, err } - row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp), obj.Spec.Username, status) + signerName := "" + if obj.Spec.SignerName != "" { + signerName = obj.Spec.SignerName + } + row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp), signerName, obj.Spec.Username, status) return []metav1.TableRow{row}, nil } diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index ff73ae6c10e..f4e3fa2a79d 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -3417,7 +3417,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) { Status: certificates.CertificateSigningRequestStatus{}, }, // Columns: Name, Age, Requestor, Condition - expected: []metav1.TableRow{{Cells: []interface{}{"csr1", "0s", "", "Pending"}}}, + expected: []metav1.TableRow{{Cells: []interface{}{"csr1", "0s", "", "", "Pending"}}}, }, // Basic CSR with Spec and Status=Approved. { @@ -3438,7 +3438,29 @@ func TestPrintCertificateSigningRequest(t *testing.T) { }, }, // Columns: Name, Age, Requestor, Condition - expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "CSR Requestor", "Approved"}}}, + expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "", "CSR Requestor", "Approved"}}}, + }, + // Basic CSR with Spec and SignerName set + { + csr: certificates.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "csr2", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Spec: certificates.CertificateSigningRequestSpec{ + Username: "CSR Requestor", + SignerName: "example.com/test-signer", + }, + Status: certificates.CertificateSigningRequestStatus{ + Conditions: []certificates.CertificateSigningRequestCondition{ + { + Type: certificates.CertificateApproved, + }, + }, + }, + }, + // Columns: Name, Age, Requestor, Condition + expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "example.com/test-signer", "CSR Requestor", "Approved"}}}, }, // Basic CSR with Spec and Status=Approved; certificate issued. { @@ -3460,7 +3482,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) { }, }, // Columns: Name, Age, Requestor, Condition - expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "CSR Requestor", "Approved,Issued"}}}, + expected: []metav1.TableRow{{Cells: []interface{}{"csr2", "0s", "", "CSR Requestor", "Approved,Issued"}}}, }, // Basic CSR with Spec and Status=Denied. { @@ -3481,7 +3503,7 @@ func TestPrintCertificateSigningRequest(t *testing.T) { }, }, // Columns: Name, Age, Requestor, Condition - expected: []metav1.TableRow{{Cells: []interface{}{"csr3", "0s", "CSR Requestor", "Denied"}}}, + expected: []metav1.TableRow{{Cells: []interface{}{"csr3", "0s", "", "CSR Requestor", "Denied"}}}, }, } diff --git a/pkg/registry/certificates/certificates/BUILD b/pkg/registry/certificates/certificates/BUILD index d83adb9bbd8..7c4fc9febbc 100644 --- a/pkg/registry/certificates/certificates/BUILD +++ b/pkg/registry/certificates/certificates/BUILD @@ -18,9 +18,12 @@ go_library( "//pkg/apis/certificates:go_default_library", "//pkg/apis/certificates/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library", ], ) diff --git a/pkg/registry/certificates/certificates/storage/storage.go b/pkg/registry/certificates/certificates/storage/storage.go index cda697fda55..78d649b5257 100644 --- a/pkg/registry/certificates/certificates/storage/storage.go +++ b/pkg/registry/certificates/certificates/storage/storage.go @@ -50,7 +50,7 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, *Approva TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, } - options := &generic.StoreOptions{RESTOptions: optsGetter} + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: csrregistry.GetAttrs} if err := store.CompleteWithOptions(options); err != nil { return nil, nil, nil, err } diff --git a/pkg/registry/certificates/certificates/strategy.go b/pkg/registry/certificates/certificates/strategy.go index 7bfbd537532..1bbebdedde9 100644 --- a/pkg/registry/certificates/certificates/strategy.go +++ b/pkg/registry/certificates/certificates/strategy.go @@ -21,9 +21,12 @@ import ( "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/storage/names" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/certificates" @@ -190,3 +193,21 @@ func (csrApprovalStrategy) PrepareForUpdate(ctx context.Context, obj, old runtim func (csrApprovalStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { return validation.ValidateCertificateSigningRequestUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest)) } + +// GetAttrs returns labels and fields of a given object for filtering purposes. +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + csr, ok := obj.(*certificates.CertificateSigningRequest) + if !ok { + return nil, nil, fmt.Errorf("not a certificatesigningrequest") + } + return labels.Set(csr.Labels), SelectableFields(csr), nil +} + +// SelectableFields returns a field set that can be used for filter selection +func SelectableFields(obj *certificates.CertificateSigningRequest) fields.Set { + objectMetaFieldsSet := generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false) + csrSpecificFieldsSet := fields.Set{ + "spec.signerName": obj.Spec.SignerName, + } + return generic.MergeFieldsSets(objectMetaFieldsSet, csrSpecificFieldsSet) +} diff --git a/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go b/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go index 301aefc2d39..24fa4bf8103 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go @@ -227,58 +227,59 @@ func init() { } var fileDescriptor_09d156762b8218ef = []byte{ - // 805 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4b, 0x8f, 0x1b, 0x45, - 0x10, 0xf6, 0xf8, 0xb5, 0x76, 0x7b, 0xd9, 0x44, 0x2d, 0x14, 0x0d, 0x2b, 0x65, 0x66, 0x35, 0x02, - 0xb4, 0x3c, 0xd2, 0xc3, 0x46, 0x08, 0x56, 0x7b, 0x40, 0x30, 0x4b, 0x04, 0x2b, 0x12, 0x21, 0x75, - 0x62, 0x0e, 0x08, 0x89, 0xb4, 0xc7, 0x95, 0x71, 0xc7, 0x99, 0x07, 0xd3, 0x3d, 0x06, 0xdf, 0xf2, - 0x13, 0x38, 0x72, 0x41, 0xe2, 0x97, 0x70, 0x5e, 0x0e, 0x48, 0x39, 0xe6, 0x80, 0x2c, 0xd6, 0xfc, - 0x8b, 0x9c, 0x50, 0xf7, 0xb4, 0x3d, 0xc6, 0x2b, 0xe3, 0x28, 0x7b, 0x9b, 0xfa, 0xaa, 0xbe, 0xaf, - 0x1e, 0x5d, 0x35, 0xe8, 0xcb, 0xf1, 0xb1, 0x20, 0x3c, 0xf5, 0xc7, 0xc5, 0x00, 0xf2, 0x04, 0x24, - 0x08, 0x7f, 0x02, 0xc9, 0x30, 0xcd, 0x7d, 0xe3, 0x60, 0x19, 0xf7, 0x43, 0xc8, 0x25, 0x7f, 0xc4, - 0x43, 0xa6, 0xdd, 0x47, 0x03, 0x90, 0xec, 0xc8, 0x8f, 0x20, 0x81, 0x9c, 0x49, 0x18, 0x92, 0x2c, - 0x4f, 0x65, 0x8a, 0xdd, 0x92, 0x40, 0x58, 0xc6, 0xc9, 0x2a, 0x81, 0x18, 0xc2, 0xfe, 0xad, 0x88, - 0xcb, 0x51, 0x31, 0x20, 0x61, 0x1a, 0xfb, 0x51, 0x1a, 0xa5, 0xbe, 0xe6, 0x0d, 0x8a, 0x47, 0xda, - 0xd2, 0x86, 0xfe, 0x2a, 0xf5, 0xf6, 0x3f, 0xac, 0x0a, 0x88, 0x59, 0x38, 0xe2, 0x09, 0xe4, 0x53, - 0x3f, 0x1b, 0x47, 0x0a, 0x10, 0x7e, 0x0c, 0x92, 0xf9, 0x93, 0x4b, 0x55, 0xec, 0xfb, 0x9b, 0x58, - 0x79, 0x91, 0x48, 0x1e, 0xc3, 0x25, 0xc2, 0x47, 0xdb, 0x08, 0x22, 0x1c, 0x41, 0xcc, 0xd6, 0x79, - 0xde, 0x1f, 0x75, 0xf4, 0xc6, 0x69, 0xd5, 0xe6, 0x7d, 0x1e, 0x25, 0x3c, 0x89, 0x28, 0xfc, 0x50, - 0x80, 0x90, 0xf8, 0x21, 0xea, 0xa8, 0x0a, 0x87, 0x4c, 0x32, 0xdb, 0x3a, 0xb0, 0x0e, 0x7b, 0xb7, - 0x3f, 0x20, 0xd5, 0x7c, 0x96, 0x89, 0x48, 0x36, 0x8e, 0x14, 0x20, 0x88, 0x8a, 0x26, 0x93, 0x23, - 0xf2, 0xf5, 0xe0, 0x31, 0x84, 0xf2, 0x1e, 0x48, 0x16, 0xe0, 0xf3, 0x99, 0x5b, 0x9b, 0xcf, 0x5c, - 0x54, 0x61, 0x74, 0xa9, 0x8a, 0x1f, 0xa2, 0xa6, 0xc8, 0x20, 0xb4, 0xeb, 0x5a, 0xfd, 0x13, 0xb2, - 0x65, 0xfa, 0x64, 0x63, 0xad, 0xf7, 0x33, 0x08, 0x83, 0x5d, 0x93, 0xab, 0xa9, 0x2c, 0xaa, 0x95, - 0xf1, 0x08, 0xb5, 0x85, 0x64, 0xb2, 0x10, 0x76, 0x43, 0xe7, 0xf8, 0xf4, 0x0a, 0x39, 0xb4, 0x4e, - 0xb0, 0x67, 0xb2, 0xb4, 0x4b, 0x9b, 0x1a, 0x7d, 0xef, 0xd7, 0x3a, 0xf2, 0x36, 0x72, 0x4f, 0xd3, - 0x64, 0xc8, 0x25, 0x4f, 0x13, 0x7c, 0x8c, 0x9a, 0x72, 0x9a, 0x81, 0x1e, 0x68, 0x37, 0x78, 0x73, - 0x51, 0xf2, 0x83, 0x69, 0x06, 0x2f, 0x66, 0xee, 0xeb, 0xeb, 0xf1, 0x0a, 0xa7, 0x9a, 0x81, 0xdf, - 0x46, 0xed, 0x1c, 0x98, 0x48, 0x13, 0x3d, 0xae, 0x6e, 0x55, 0x08, 0xd5, 0x28, 0x35, 0x5e, 0xfc, - 0x0e, 0xda, 0x89, 0x41, 0x08, 0x16, 0x81, 0xee, 0xb9, 0x1b, 0x5c, 0x33, 0x81, 0x3b, 0xf7, 0x4a, - 0x98, 0x2e, 0xfc, 0xf8, 0x31, 0xda, 0x7b, 0xc2, 0x84, 0xec, 0x67, 0x43, 0x26, 0xe1, 0x01, 0x8f, - 0xc1, 0x6e, 0xea, 0x29, 0xbd, 0xfb, 0x72, 0xef, 0xac, 0x18, 0xc1, 0x0d, 0xa3, 0xbe, 0x77, 0xf7, - 0x3f, 0x4a, 0x74, 0x4d, 0xd9, 0x9b, 0x59, 0xe8, 0xe6, 0xc6, 0xf9, 0xdc, 0xe5, 0x42, 0xe2, 0xef, - 0x2e, 0xed, 0x1b, 0x79, 0xb9, 0x3a, 0x14, 0x5b, 0x6f, 0xdb, 0x75, 0x53, 0x4b, 0x67, 0x81, 0xac, - 0xec, 0xda, 0xf7, 0xa8, 0xc5, 0x25, 0xc4, 0xc2, 0xae, 0x1f, 0x34, 0x0e, 0x7b, 0xb7, 0x4f, 0x5e, - 0x7d, 0x11, 0x82, 0xd7, 0x4c, 0x9a, 0xd6, 0x99, 0x12, 0xa4, 0xa5, 0xae, 0xf7, 0x7b, 0xe3, 0x7f, - 0x1a, 0x54, 0x2b, 0x89, 0xdf, 0x42, 0x3b, 0x79, 0x69, 0xea, 0xfe, 0x76, 0x83, 0x9e, 0x7a, 0x15, - 0x13, 0x41, 0x17, 0x3e, 0x4c, 0x50, 0xbb, 0x50, 0xcf, 0x23, 0xec, 0xd6, 0x41, 0xe3, 0xb0, 0x1b, - 0xdc, 0x50, 0x8f, 0xdc, 0xd7, 0xc8, 0x8b, 0x99, 0xdb, 0xf9, 0x0a, 0xa6, 0xda, 0xa0, 0x26, 0x0a, - 0xbf, 0x8f, 0x3a, 0x85, 0x80, 0x3c, 0x61, 0x31, 0x98, 0xd5, 0x58, 0xce, 0xa1, 0x6f, 0x70, 0xba, - 0x8c, 0xc0, 0x37, 0x51, 0xa3, 0xe0, 0x43, 0xb3, 0x1a, 0x3d, 0x13, 0xd8, 0xe8, 0x9f, 0x7d, 0x4e, - 0x15, 0x8e, 0x3d, 0xd4, 0x8e, 0xf2, 0xb4, 0xc8, 0x84, 0xdd, 0xd4, 0xc9, 0x91, 0x4a, 0xfe, 0x85, - 0x46, 0xa8, 0xf1, 0xe0, 0x04, 0xb5, 0xe0, 0x27, 0x99, 0x33, 0xbb, 0xad, 0x47, 0x79, 0x76, 0xb5, - 0xbb, 0x25, 0x77, 0x94, 0xd6, 0x9d, 0x44, 0xe6, 0xd3, 0x6a, 0xb2, 0x1a, 0xa3, 0x65, 0x9a, 0x7d, - 0x40, 0xa8, 0x8a, 0xc1, 0xd7, 0x51, 0x63, 0x0c, 0xd3, 0xf2, 0x80, 0xa8, 0xfa, 0xc4, 0x9f, 0xa1, - 0xd6, 0x84, 0x3d, 0x29, 0xc0, 0xfc, 0x47, 0xde, 0xdb, 0x5a, 0x8f, 0x56, 0xfb, 0x46, 0x51, 0x68, - 0xc9, 0x3c, 0xa9, 0x1f, 0x5b, 0xde, 0x9f, 0x16, 0x72, 0xb7, 0x5c, 0x3f, 0xfe, 0x11, 0xa1, 0x70, - 0x71, 0x9b, 0xc2, 0xb6, 0x74, 0xff, 0xa7, 0xaf, 0xde, 0xff, 0xf2, 0xce, 0xab, 0x1f, 0xe5, 0x12, - 0x12, 0x74, 0x25, 0x15, 0x3e, 0x42, 0xbd, 0x15, 0x69, 0xdd, 0xe9, 0x6e, 0x70, 0x6d, 0x3e, 0x73, - 0x7b, 0x2b, 0xe2, 0x74, 0x35, 0xc6, 0xfb, 0xd8, 0x8c, 0x4d, 0x37, 0x8a, 0xdd, 0xc5, 0xfe, 0x5b, - 0xfa, 0x5d, 0xbb, 0xeb, 0xfb, 0x7b, 0xd2, 0xf9, 0xe5, 0x37, 0xb7, 0xf6, 0xf4, 0xaf, 0x83, 0x5a, - 0x70, 0xeb, 0xfc, 0xc2, 0xa9, 0x3d, 0xbb, 0x70, 0x6a, 0xcf, 0x2f, 0x9c, 0xda, 0xd3, 0xb9, 0x63, - 0x9d, 0xcf, 0x1d, 0xeb, 0xd9, 0xdc, 0xb1, 0x9e, 0xcf, 0x1d, 0xeb, 0xef, 0xb9, 0x63, 0xfd, 0xfc, - 0x8f, 0x53, 0xfb, 0x76, 0xc7, 0x74, 0xf7, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x39, 0x0e, 0xb6, - 0xcd, 0x7f, 0x07, 0x00, 0x00, + // 824 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4d, 0x6f, 0x1b, 0x45, + 0x18, 0xf6, 0xfa, 0xdb, 0xe3, 0x90, 0x56, 0x23, 0x54, 0x2d, 0x91, 0xba, 0x1b, 0xad, 0x00, 0x85, + 0x8f, 0xce, 0x92, 0x0a, 0x41, 0x94, 0x03, 0x82, 0x0d, 0x15, 0x44, 0xb4, 0x20, 0x4d, 0x1a, 0x0e, + 0x08, 0x89, 0x8e, 0xd7, 0x6f, 0x37, 0x53, 0x77, 0x3f, 0xd8, 0x99, 0x35, 0xf8, 0xd6, 0x9f, 0xc0, + 0x91, 0x0b, 0x12, 0x3f, 0x27, 0x1c, 0x90, 0x7a, 0xec, 0x01, 0x59, 0xc4, 0xdc, 0xf9, 0x01, 0x3d, + 0xa1, 0x99, 0x1d, 0x7b, 0x8d, 0x23, 0xd7, 0x55, 0x73, 0xdb, 0xf7, 0x79, 0xdf, 0xe7, 0x79, 0x3f, + 0x67, 0xd1, 0x97, 0xa3, 0x03, 0x41, 0x78, 0xea, 0x8f, 0x8a, 0x01, 0xe4, 0x09, 0x48, 0x10, 0xfe, + 0x18, 0x92, 0x61, 0x9a, 0xfb, 0xc6, 0xc1, 0x32, 0xee, 0x87, 0x90, 0x4b, 0xfe, 0x90, 0x87, 0x4c, + 0xbb, 0xf7, 0x07, 0x20, 0xd9, 0xbe, 0x1f, 0x41, 0x02, 0x39, 0x93, 0x30, 0x24, 0x59, 0x9e, 0xca, + 0x14, 0xbb, 0x25, 0x81, 0xb0, 0x8c, 0x93, 0x65, 0x02, 0x31, 0x84, 0x9d, 0x5b, 0x11, 0x97, 0x67, + 0xc5, 0x80, 0x84, 0x69, 0xec, 0x47, 0x69, 0x94, 0xfa, 0x9a, 0x37, 0x28, 0x1e, 0x6a, 0x4b, 0x1b, + 0xfa, 0xab, 0xd4, 0xdb, 0xf9, 0xb0, 0x2a, 0x20, 0x66, 0xe1, 0x19, 0x4f, 0x20, 0x9f, 0xf8, 0xd9, + 0x28, 0x52, 0x80, 0xf0, 0x63, 0x90, 0xcc, 0x1f, 0x5f, 0xaa, 0x62, 0xc7, 0x5f, 0xc7, 0xca, 0x8b, + 0x44, 0xf2, 0x18, 0x2e, 0x11, 0x3e, 0xda, 0x44, 0x10, 0xe1, 0x19, 0xc4, 0x6c, 0x95, 0xe7, 0xfd, + 0x51, 0x47, 0x6f, 0x1c, 0x55, 0x6d, 0x9e, 0xf0, 0x28, 0xe1, 0x49, 0x44, 0xe1, 0xc7, 0x02, 0x84, + 0xc4, 0x0f, 0x50, 0x57, 0x55, 0x38, 0x64, 0x92, 0xd9, 0xd6, 0xae, 0xb5, 0xd7, 0xbf, 0xfd, 0x01, + 0xa9, 0xe6, 0xb3, 0x48, 0x44, 0xb2, 0x51, 0xa4, 0x00, 0x41, 0x54, 0x34, 0x19, 0xef, 0x93, 0x6f, + 0x06, 0x8f, 0x20, 0x94, 0xf7, 0x40, 0xb2, 0x00, 0x9f, 0x4f, 0xdd, 0xda, 0x6c, 0xea, 0xa2, 0x0a, + 0xa3, 0x0b, 0x55, 0xfc, 0x00, 0x35, 0x45, 0x06, 0xa1, 0x5d, 0xd7, 0xea, 0x9f, 0x90, 0x0d, 0xd3, + 0x27, 0x6b, 0x6b, 0x3d, 0xc9, 0x20, 0x0c, 0xb6, 0x4c, 0xae, 0xa6, 0xb2, 0xa8, 0x56, 0xc6, 0x67, + 0xa8, 0x2d, 0x24, 0x93, 0x85, 0xb0, 0x1b, 0x3a, 0xc7, 0xa7, 0x57, 0xc8, 0xa1, 0x75, 0x82, 0x6d, + 0x93, 0xa5, 0x5d, 0xda, 0xd4, 0xe8, 0x7b, 0xbf, 0xd5, 0x91, 0xb7, 0x96, 0x7b, 0x94, 0x26, 0x43, + 0x2e, 0x79, 0x9a, 0xe0, 0x03, 0xd4, 0x94, 0x93, 0x0c, 0xf4, 0x40, 0x7b, 0xc1, 0x9b, 0xf3, 0x92, + 0xef, 0x4f, 0x32, 0x78, 0x3e, 0x75, 0x5f, 0x5f, 0x8d, 0x57, 0x38, 0xd5, 0x0c, 0xfc, 0x36, 0x6a, + 0xe7, 0xc0, 0x44, 0x9a, 0xe8, 0x71, 0xf5, 0xaa, 0x42, 0xa8, 0x46, 0xa9, 0xf1, 0xe2, 0x77, 0x50, + 0x27, 0x06, 0x21, 0x58, 0x04, 0xba, 0xe7, 0x5e, 0x70, 0xcd, 0x04, 0x76, 0xee, 0x95, 0x30, 0x9d, + 0xfb, 0xf1, 0x23, 0xb4, 0xfd, 0x98, 0x09, 0x79, 0x9a, 0x0d, 0x99, 0x84, 0xfb, 0x3c, 0x06, 0xbb, + 0xa9, 0xa7, 0xf4, 0xee, 0xcb, 0xed, 0x59, 0x31, 0x82, 0x1b, 0x46, 0x7d, 0xfb, 0xee, 0xff, 0x94, + 0xe8, 0x8a, 0xb2, 0x37, 0xb5, 0xd0, 0xcd, 0xb5, 0xf3, 0xb9, 0xcb, 0x85, 0xc4, 0xdf, 0x5f, 0xba, + 0x37, 0xf2, 0x72, 0x75, 0x28, 0xb6, 0xbe, 0xb6, 0xeb, 0xa6, 0x96, 0xee, 0x1c, 0x59, 0xba, 0xb5, + 0x1f, 0x50, 0x8b, 0x4b, 0x88, 0x85, 0x5d, 0xdf, 0x6d, 0xec, 0xf5, 0x6f, 0x1f, 0xbe, 0xfa, 0x21, + 0x04, 0xaf, 0x99, 0x34, 0xad, 0x63, 0x25, 0x48, 0x4b, 0x5d, 0xef, 0xdf, 0xc6, 0x0b, 0x1a, 0x54, + 0x27, 0x89, 0xdf, 0x42, 0x9d, 0xbc, 0x34, 0x75, 0x7f, 0x5b, 0x41, 0x5f, 0x6d, 0xc5, 0x44, 0xd0, + 0xb9, 0x0f, 0x13, 0x84, 0x04, 0x8f, 0x12, 0xc8, 0xbf, 0x66, 0x31, 0xd8, 0x9d, 0x72, 0xd9, 0xea, + 0x0d, 0x9d, 0x2c, 0x50, 0xba, 0x14, 0x81, 0x09, 0x6a, 0x17, 0x6a, 0x9d, 0xc2, 0x6e, 0xed, 0x36, + 0xf6, 0x7a, 0xc1, 0x0d, 0x75, 0x14, 0xa7, 0x1a, 0x79, 0x3e, 0x75, 0xbb, 0x5f, 0xc1, 0x44, 0x1b, + 0xd4, 0x44, 0xe1, 0xf7, 0x51, 0xb7, 0x10, 0x90, 0x27, 0x4a, 0xbd, 0x3c, 0xa5, 0xc5, 0xdc, 0x4e, + 0x0d, 0x4e, 0x17, 0x11, 0xf8, 0x26, 0x6a, 0x14, 0x7c, 0x68, 0x4e, 0xa9, 0x6f, 0x02, 0x1b, 0xa7, + 0xc7, 0x9f, 0x53, 0x85, 0x63, 0x0f, 0xb5, 0xa3, 0x3c, 0x2d, 0x32, 0x61, 0x37, 0x75, 0x72, 0xa4, + 0x92, 0x7f, 0xa1, 0x11, 0x6a, 0x3c, 0x38, 0x41, 0x2d, 0xf8, 0x59, 0xe6, 0xcc, 0x6e, 0xeb, 0xd1, + 0x1f, 0x5f, 0xed, 0x9d, 0x93, 0x3b, 0x4a, 0xeb, 0x4e, 0x22, 0xf3, 0x49, 0xb5, 0x09, 0x8d, 0xd1, + 0x32, 0xcd, 0x0e, 0x20, 0x54, 0xc5, 0xe0, 0xeb, 0xa8, 0x31, 0x82, 0x49, 0xf9, 0xe0, 0xa8, 0xfa, + 0xc4, 0x9f, 0xa1, 0xd6, 0x98, 0x3d, 0x2e, 0xc0, 0xfc, 0x77, 0xde, 0xdb, 0x58, 0x8f, 0x56, 0xfb, + 0x56, 0x51, 0x68, 0xc9, 0x3c, 0xac, 0x1f, 0x58, 0xde, 0x9f, 0x16, 0x72, 0x37, 0xfc, 0x2d, 0xf0, + 0x4f, 0x08, 0x85, 0xf3, 0xb7, 0x2c, 0x6c, 0x4b, 0xf7, 0x7f, 0xf4, 0xea, 0xfd, 0x2f, 0xfe, 0x0b, + 0xd5, 0x8f, 0x75, 0x01, 0x09, 0xba, 0x94, 0x0a, 0xef, 0xa3, 0xfe, 0x92, 0xb4, 0xee, 0x74, 0x2b, + 0xb8, 0x36, 0x9b, 0xba, 0xfd, 0x25, 0x71, 0xba, 0x1c, 0xe3, 0x7d, 0x6c, 0xc6, 0xa6, 0x1b, 0xc5, + 0xee, 0xfc, 0xbd, 0x58, 0x7a, 0xaf, 0xbd, 0xd5, 0x7b, 0x3f, 0xec, 0xfe, 0xfa, 0xbb, 0x5b, 0x7b, + 0xf2, 0xd7, 0x6e, 0x2d, 0xb8, 0x75, 0x7e, 0xe1, 0xd4, 0x9e, 0x5e, 0x38, 0xb5, 0x67, 0x17, 0x4e, + 0xed, 0xc9, 0xcc, 0xb1, 0xce, 0x67, 0x8e, 0xf5, 0x74, 0xe6, 0x58, 0xcf, 0x66, 0x8e, 0xf5, 0xf7, + 0xcc, 0xb1, 0x7e, 0xf9, 0xc7, 0xa9, 0x7d, 0xd7, 0x31, 0xdd, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, + 0x69, 0x8d, 0xc8, 0xd3, 0xaf, 0x07, 0x00, 0x00, } func (m *CertificateSigningRequest) Marshal() (dAtA []byte, err error) { @@ -449,6 +450,13 @@ func (m *CertificateSigningRequestSpec) MarshalToSizedBuffer(dAtA []byte) (int, _ = i var l int _ = l + if m.SignerName != nil { + i -= len(*m.SignerName) + copy(dAtA[i:], *m.SignerName) + i = encodeVarintGenerated(dAtA, i, uint64(len(*m.SignerName))) + i-- + dAtA[i] = 0x3a + } if len(m.Extra) > 0 { keysForExtra := make([]string, 0, len(m.Extra)) for k := range m.Extra { @@ -687,6 +695,10 @@ func (m *CertificateSigningRequestSpec) Size() (n int) { n += mapEntrySize + 1 + sovGenerated(uint64(mapEntrySize)) } } + if m.SignerName != nil { + l = len(*m.SignerName) + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -792,6 +804,7 @@ func (this *CertificateSigningRequestSpec) String() string { `Groups:` + fmt.Sprintf("%v", this.Groups) + `,`, `Usages:` + fmt.Sprintf("%v", this.Usages) + `,`, `Extra:` + mapStringForExtra + `,`, + `SignerName:` + valueToStringGenerated(this.SignerName) + `,`, `}`, }, "") return s @@ -1594,6 +1607,39 @@ func (m *CertificateSigningRequestSpec) Unmarshal(dAtA []byte) error { } m.Extra[mapkey] = *mapvalue iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SignerName", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(dAtA[iNdEx:postIndex]) + m.SignerName = &s + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto index 5200224a2ff..78d2dbc78fb 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto @@ -73,6 +73,19 @@ message CertificateSigningRequestSpec { // Base64-encoded PKCS#10 CSR data optional bytes request = 1; + // Requested signer for the request. It is a qualified name in the form: + // `scope-hostname.io/name`. + // If empty, it will be defaulted: + // 1. If it's a kubelet client certificate, it is assigned + // "kubernetes.io/kube-apiserver-client-kubelet". + // 2. If it's a kubelet serving certificate, it is assigned + // "kubernetes.io/kubelet-serving". + // 3. Otherwise, it is assigned "kubernetes.io/legacy-unknown". + // Distribution of trust for signers happens out of band. + // You can select on this field using `spec.signerName`. + // +optional + optional string signerName = 7; + // allowedUsages specifies a set of usage contexts the key will be // valid for. // See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types.go b/staging/src/k8s.io/api/certificates/v1beta1/types.go index 93f81cd529b..5a46e63420f 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types.go @@ -48,6 +48,19 @@ type CertificateSigningRequestSpec struct { // Base64-encoded PKCS#10 CSR data Request []byte `json:"request" protobuf:"bytes,1,opt,name=request"` + // Requested signer for the request. It is a qualified name in the form: + // `scope-hostname.io/name`. + // If empty, it will be defaulted: + // 1. If it's a kubelet client certificate, it is assigned + // "kubernetes.io/kube-apiserver-client-kubelet". + // 2. If it's a kubelet serving certificate, it is assigned + // "kubernetes.io/kubelet-serving". + // 3. Otherwise, it is assigned "kubernetes.io/legacy-unknown". + // Distribution of trust for signers happens out of band. + // You can select on this field using `spec.signerName`. + // +optional + SignerName *string `json:"signerName,omitempty" protobuf:"bytes,7,opt,name=signerName"` + // allowedUsages specifies a set of usage contexts the key will be // valid for. // See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3 @@ -72,6 +85,28 @@ type CertificateSigningRequestSpec struct { Extra map[string]ExtraValue `json:"extra,omitempty" protobuf:"bytes,6,rep,name=extra"` } +// Built in signerName values that are honoured by kube-controller-manager. +// None of these usages are related to ServiceAccount token secrets +// `.data[ca.crt]` in any way. +const ( + // Signs certificates that will be honored as client-certs by the + // kube-apiserver. Never auto-approved by kube-controller-manager. + KubeAPIServerClientSignerName = "kubernetes.io/kube-apiserver-client" + + // Signs client certificates that will be honored as client-certs by the + // kube-apiserver for a kubelet. + // May be auto-approved by kube-controller-manager. + KubeAPIServerClientKubeletSignerName = "kubernetes.io/kube-apiserver-client-kubelet" + + // Signs serving certificates that are honored as a valid kubelet serving + // certificate by the kube-apiserver, but has no other guarantees. + KubeletServingSignerName = "kubernetes.io/kubelet-serving" + + // Has no guarantees for trust at all. Some distributions may honor these + // as client certs, but that behavior is not standard kubernetes behavior. + LegacyUnknownSignerName = "kubernetes.io/legacy-unknown" +) + // ExtraValue masks the value so protobuf can generate // +protobuf.nullable=true // +protobuf.options.(gogoproto.goproto_stringer)=false diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go index f6a7e16acb0..a2edb45a815 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go @@ -49,13 +49,14 @@ func (CertificateSigningRequestCondition) SwaggerDoc() map[string]string { } var map_CertificateSigningRequestSpec = map[string]string{ - "": "This information is immutable after the request is created. Only the Request and Usages fields can be set on creation, other fields are derived by Kubernetes and cannot be modified by users.", - "request": "Base64-encoded PKCS#10 CSR data", - "usages": "allowedUsages specifies a set of usage contexts the key will be valid for. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3\n https://tools.ietf.org/html/rfc5280#section-4.2.1.12", - "username": "Information about the requesting user. See user.Info interface for details.", - "uid": "UID information about the requesting user. See user.Info interface for details.", - "groups": "Group information about the requesting user. See user.Info interface for details.", - "extra": "Extra information about the requesting user. See user.Info interface for details.", + "": "This information is immutable after the request is created. Only the Request and Usages fields can be set on creation, other fields are derived by Kubernetes and cannot be modified by users.", + "request": "Base64-encoded PKCS#10 CSR data", + "signerName": "Requested signer for the request. It is a qualified name in the form: `scope-hostname.io/name`. If empty, it will be defaulted:\n 1. If it's a kubelet client certificate, it is assigned\n \"kubernetes.io/kube-apiserver-client-kubelet\".\n 2. If it's a kubelet serving certificate, it is assigned\n \"kubernetes.io/kubelet-serving\".\n 3. Otherwise, it is assigned \"kubernetes.io/legacy-unknown\".\nDistribution of trust for signers happens out of band. You can select on this field using `spec.signerName`.", + "usages": "allowedUsages specifies a set of usage contexts the key will be valid for. See: https://tools.ietf.org/html/rfc5280#section-4.2.1.3\n https://tools.ietf.org/html/rfc5280#section-4.2.1.12", + "username": "Information about the requesting user. See user.Info interface for details.", + "uid": "UID information about the requesting user. See user.Info interface for details.", + "groups": "Group information about the requesting user. See user.Info interface for details.", + "extra": "Extra information about the requesting user. See user.Info interface for details.", } func (CertificateSigningRequestSpec) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go index b3e0aeb5073..11d0f77dd91 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go @@ -110,6 +110,11 @@ func (in *CertificateSigningRequestSpec) DeepCopyInto(out *CertificateSigningReq *out = make([]byte, len(*in)) copy(*out, *in) } + if in.SignerName != nil { + in, out := &in.SignerName, &out.SignerName + *out = new(string) + **out = **in + } if in.Usages != nil { in, out := &in.Usages, &out.Usages *out = make([]KeyUsage, len(*in)) diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json index 8b5940d5698..a801b472921 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json +++ b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json @@ -42,17 +42,18 @@ }, "spec": { "request": "OA==", + "signerName": "19", "usages": [ "J枊a" ], - "username": "19", - "uid": "20", + "username": "20", + "uid": "21", "groups": [ - "21" + "22" ], "extra": { - "22": [ - "23" + "23": [ + "24" ] } }, @@ -60,8 +61,8 @@ "conditions": [ { "type": "o,c鮽ort昍řČ扷5Ɨ", - "reason": "24", - "message": "25", + "reason": "25", + "message": "26", "lastUpdateTime": "2901-11-14T22:54:07Z" } ], diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb index c04fd179299..4ee509f46f5 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb and b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml index 166322dcd90..c24c1ff0c80 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml @@ -31,19 +31,20 @@ metadata: uid: "7" spec: extra: - "22": - - "23" + "23": + - "24" groups: - - "21" + - "22" request: OA== - uid: "20" + signerName: "19" + uid: "21" usages: - J枊a - username: "19" + username: "20" status: certificate: 9Q== conditions: - lastUpdateTime: "2901-11-14T22:54:07Z" - message: "25" - reason: "24" + message: "26" + reason: "25" type: o,c鮽ort昍řČ扷5Ɨ diff --git a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go index 586420df066..d39ddfbc687 100644 --- a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go +++ b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go @@ -3239,6 +3239,9 @@ func describeCertificateSigningRequest(csr *certificatesv1beta1.CertificateSigni w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(csr.Annotations)) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", csr.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Requesting User:\t%s\n", csr.Spec.Username) + if csr.Spec.SignerName != nil { + w.Write(LEVEL_0, "Signer:\t%s\n", *csr.Spec.SignerName) + } w.Write(LEVEL_0, "Status:\t%s\n", status) w.Write(LEVEL_0, "Subject:\n") diff --git a/test/integration/BUILD b/test/integration/BUILD index a9b15219ded..b2659d49c7f 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -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", diff --git a/test/integration/certificates/BUILD b/test/integration/certificates/BUILD new file mode 100644 index 00000000000..d9eb36ac965 --- /dev/null +++ b/test/integration/certificates/BUILD @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "go_default_test", + srcs = [ + "defaulting_test.go", + "field_selector_test.go", + "main_test.go", + ], + tags = ["integration"], + deps = [ + "//staging/src/k8s.io/api/certificates/v1beta1: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/client-go/kubernetes: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"], +) diff --git a/test/integration/certificates/defaulting_test.go b/test/integration/certificates/defaulting_test.go new file mode 100644 index 00000000000..4e448f0d01c --- /dev/null +++ b/test/integration/certificates/defaulting_test.go @@ -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: testCSRPEM, + Usages: []capi.KeyUsage{capi.UsageKeyEncipherment, capi.UsageDigitalSignature}, + }, + expectedSignerName: capi.LegacyUnknownSignerName, + }, + "does not default signerName if an explicit value is provided": { + csr: capi.CertificateSigningRequestSpec{ + Request: testCSRPEM, + 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) + } + }) + } +} diff --git a/test/integration/certificates/field_selector_test.go b/test/integration/certificates/field_selector_test.go new file mode 100644 index 00000000000..36b7e78e177 --- /dev/null +++ b/test/integration/certificates/field_selector_test.go @@ -0,0 +1,112 @@ +/* +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" + 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 string) *capi.CertificateSigningRequest { + csr, err := certClient.Create(context.TODO(), buildTestingCSR(name, signerName), metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create testing CSR: %v", err) + } + return csr +} + +func buildTestingCSR(name, signerName string) *capi.CertificateSigningRequest { + return &capi.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: capi.CertificateSigningRequestSpec{ + SignerName: &signerName, + Request: testCSRPEM, + }, + } +} + +var ( + // The contents of this CSR do not matter, and it is only used to allow the + // CSR resource submitted during integration tests to pass through + // validation. + testCSRPEM = []byte(`-----BEGIN CERTIFICATE REQUEST----- +MIICrzCCAZcCAQAwajENMAsGA1UECAwESE9OSzENMAsGA1UEBwwESE9OSzENMAsG +A1UECgwESE9OSzENMAsGA1UECwwESE9OSzENMAsGA1UEAwwESE9OSzEdMBsGCSqG +SIb3DQEJARYOaG9ua0Bob25rLmhvbmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDhw6t5C0Vtzzl4jQVMM9S2epAKOyKZCXRYC50sG8UFectfSALJHPUY +rv3LNfUTSkqg+EJO+5an1PeQS+GK94DUiJ2cUR2hBiTfXenyDAm2fGSDIqLQ/YcZ +fprwlqMu3YfpMH1KyyNORoOgWgsyWP0rBIRoWEFcFNaBu7BazaJHQIYNpcyRkHJC +610It4MV5dUqNFAfYqmxqlkMa4lR0U4f8cCA3J+lajNOMz/GkPotBINU+xX4bVob +Q+ghAatgiZnEvC6pe0LqG788SHaIu7hArSK8ZG7+HcqCwISFLJiA8+A6HE24PhQC +69pGqHePAFO4a09c5/MTPfBfohYkEGX7AgMBAAGgADANBgkqhkiG9w0BAQsFAAOC +AQEAwg/7CWhWZICusSKEeIHJE+rgeSySAgL0S05KJKtwjHK1zf2B8Az4F2pe0aCe +r+mqNyFutmaLOXmNH7H1BJuw0wXeEg8wlT3nknRTJ4EWYf4G0H1dOICk/tB4Mgl1 +qgmMcP37QQRCMit5VY9BOKfXo+AHCH9rwmX91mXwzyejY/wO6Y3R6Y+GvMKA259F +zRt2J8VJkeeXOE/H93putfT1KcmayTwO0gTzPFd7ZZzLSVMnpirxCUujkduxy8DK +dDcZdaTZofztqa5ej1gzptxU6fBfVvl3Wevc30yDH5Dum0aiohJbijncgIR6SQx5 +6nuYWH340f/Ivm5b1gyEqb12ag== +-----END CERTIFICATE REQUEST-----`) +) diff --git a/test/integration/certificates/main_test.go b/test/integration/certificates/main_test.go new file mode 100644 index 00000000000..999f1354e96 --- /dev/null +++ b/test/integration/certificates/main_test.go @@ -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) +}