diff --git a/pkg/api/util/BUILD.bazel b/pkg/api/util/BUILD.bazel index dc14963d5..c0a591829 100644 --- a/pkg/api/util/BUILD.bazel +++ b/pkg/api/util/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "conditions.go", "duration.go", "issuers.go", + "kube.go", "names.go", "usages.go", ], @@ -15,6 +16,7 @@ go_library( "//pkg/apis/certmanager/v1:go_default_library", "//pkg/apis/meta/v1:go_default_library", "//pkg/logs:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_utils//clock:go_default_library", ], diff --git a/pkg/controller/BUILD.bazel b/pkg/controller/BUILD.bazel index f4d75cd6d..e4c431c12 100644 --- a/pkg/controller/BUILD.bazel +++ b/pkg/controller/BUILD.bazel @@ -52,6 +52,7 @@ filegroup( "//pkg/controller/cainjector:all-srcs", "//pkg/controller/certificaterequests:all-srcs", "//pkg/controller/certificates:all-srcs", + "//pkg/controller/certificatesigningrequests:all-srcs", "//pkg/controller/clusterissuers:all-srcs", "//pkg/controller/ingress-shim:all-srcs", "//pkg/controller/issuers:all-srcs", diff --git a/pkg/controller/certificatesigningrequests/BUILD.bazel b/pkg/controller/certificatesigningrequests/BUILD.bazel new file mode 100644 index 000000000..93213f800 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/BUILD.bazel @@ -0,0 +1,77 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "checks.go", + "controller.go", + "sync.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api/util:go_default_library", + "//pkg/apis/certmanager:go_default_library", + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/apis/meta/v1:go_default_library", + "//pkg/controller:go_default_library", + "//pkg/controller/certificatesigningrequests/util:go_default_library", + "//pkg/issuer:go_default_library", + "//pkg/logs:go_default_library", + "@com_github_go_logr_logr//:go_default_library", + "@io_k8s_api//authorization/v1:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", + "@io_k8s_api//core/v1:go_default_library", + "@io_k8s_apimachinery//pkg/api/errors:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/labels:go_default_library", + "@io_k8s_client_go//kubernetes/typed/authorization/v1:go_default_library", + "@io_k8s_client_go//kubernetes/typed/certificates/v1:go_default_library", + "@io_k8s_client_go//listers/certificates/v1:go_default_library", + "@io_k8s_client_go//tools/cache:go_default_library", + "@io_k8s_client_go//tools/record:go_default_library", + "@io_k8s_client_go//util/workqueue:go_default_library", + "@io_k8s_utils//clock:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["controller_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/api/util:go_default_library", + "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/apis/meta/v1:go_default_library", + "//pkg/controller:go_default_library", + "//pkg/controller/certificatesigningrequests/fake:go_default_library", + "//pkg/controller/certificatesigningrequests/util:go_default_library", + "//pkg/controller/test:go_default_library", + "//test/unit/gen:go_default_library", + "@io_k8s_api//authorization/v1:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", + "@io_k8s_api//core/v1:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_client_go//testing:go_default_library", + "@io_k8s_utils//clock/testing:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/controller/certificatesigningrequests/fake:all-srcs", + "//pkg/controller/certificatesigningrequests/util:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/controller/certificatesigningrequests/checks.go b/pkg/controller/certificatesigningrequests/checks.go new file mode 100644 index 000000000..2739b4e6e --- /dev/null +++ b/pkg/controller/certificatesigningrequests/checks.go @@ -0,0 +1,83 @@ +/* +Copyright 2021 The cert-manager 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 certificatesigningrequests + +import ( + "fmt" + + certificatesv1 "k8s.io/api/certificates/v1" + "k8s.io/apimachinery/pkg/labels" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util" + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +func (c *Controller) handleGenericIssuer(obj interface{}) { + log := c.log.WithName("handleGenericIssuer") + + iss, ok := obj.(cmapi.GenericIssuer) + if !ok { + log.Error(nil, "object does not implement GenericIssuer") + return + } + + log = logf.WithResource(log, iss) + crs, err := c.certificateSigningRequestsForGenericIssuer(iss) + if err != nil { + log.Error(err, "error looking up certificates observing issuer or clusterissuer") + return + } + for _, cr := range crs { + log := logf.WithRelatedResource(log, cr) + key, err := keyFunc(cr) + if err != nil { + log.Error(err, "error computing key for resource") + continue + } + c.queue.Add(key) + } +} + +func (c *Controller) certificateSigningRequestsForGenericIssuer(iss cmapi.GenericIssuer) ([]*certificatesv1.CertificateSigningRequest, error) { + csrs, err := c.csrLister.List(labels.NewSelector()) + if err != nil { + return nil, fmt.Errorf("error listing certificates signing requests: %s", err.Error()) + } + + _, isClusterIssuer := iss.(*cmapi.ClusterIssuer) + + var affected []*certificatesv1.CertificateSigningRequest + for _, csr := range csrs { + ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName) + + switch { + case !ok, + ref.Group != certmanager.GroupName, + iss.GetNamespace() != ref.Namespace, + iss.GetName() != ref.Name, + isClusterIssuer && ref.Type != "clusterissuers", + !isClusterIssuer && ref.Type != "issuers": + continue + } + + affected = append(affected, csr) + } + + return affected, nil +} diff --git a/pkg/controller/certificatesigningrequests/controller.go b/pkg/controller/certificatesigningrequests/controller.go new file mode 100644 index 000000000..1653fa0a2 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/controller.go @@ -0,0 +1,158 @@ +/* +Copyright 2021 The cert-manager 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 certificatesigningrequests + +import ( + "context" + + "github.com/go-logr/logr" + certificatesv1 "k8s.io/api/certificates/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + authzclient "k8s.io/client-go/kubernetes/typed/authorization/v1" + certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1" + certificateslisters "k8s.io/client-go/listers/certificates/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + "github.com/jetstack/cert-manager/pkg/issuer" + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +const ( + ControllerName = "certificatesigningrequests" +) + +var keyFunc = controllerpkg.KeyFunc + +// Signer is an implementation of a Kubernetes CertificateSigningRequest +// signer, backed by a cert-manager Issuer. +type Signer interface { + Sign(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error +} + +type Controller struct { + helper issuer.Helper + + // clientset used to update CertificateSigningRequest API resources + certClient certificatesclient.CertificateSigningRequestInterface + csrLister certificateslisters.CertificateSigningRequestLister + sarClient authzclient.SubjectAccessReviewInterface + + queue workqueue.RateLimitingInterface + + // logger to be used by this controller + log logr.Logger + + // used to record Events about resources to the API + recorder record.EventRecorder + + // Signer to call sign function + signer Signer + + // the signer kind to react to when a certificate signing request is synced + signerType string + + // used for testing + clock clock.Clock +} + +func New(signerType string, signer Signer) *Controller { + return &Controller{ + signerType: signerType, + signer: signer, + } +} + +func (c *Controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) { + // construct a new named logger to be reused throughout the controller + c.log = logf.FromContext(ctx.RootContext, ControllerName) + + // create a queue used to queue up items to be processed + c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName) + + c.sarClient = ctx.Client.AuthorizationV1().SubjectAccessReviews() + + issuerInformer := ctx.SharedInformerFactory.Certmanager().V1().Issuers() + + // obtain references to all the informers used by this controller + csrInformer := ctx.KubeSharedInformerFactory.Certificates().V1().CertificateSigningRequests() + + // build a list of InformerSynced functions that will be returned by the Register method. + // the controller will only begin processing items once all of these informers have synced. + mustSync := []cache.InformerSynced{ + csrInformer.Informer().HasSynced, + issuerInformer.Informer().HasSynced, + } + + // if scoped to a single namespace + // if we are running in non-namespaced mode (i.e. --namespace=""), we also + // register event handlers and obtain a lister for clusterissuers. + clusterIssuerInformer := ctx.SharedInformerFactory.Certmanager().V1().ClusterIssuers() + if ctx.Namespace == "" { + // register handler function for clusterissuer resources + clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + mustSync = append(mustSync, clusterIssuerInformer.Informer().HasSynced) + } + + // set all the references to the listers for used by the Sync function + c.csrLister = csrInformer.Lister() + + // register handler functions + csrInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue}) + issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer}) + + // create an issuer helper for reading generic issuers + c.helper = issuer.NewHelper(issuerInformer.Lister(), clusterIssuerInformer.Lister()) + + c.clock = ctx.Clock + // recorder records events about resources to the Kubernetes api + c.recorder = ctx.Recorder + c.certClient = ctx.Client.CertificatesV1().CertificateSigningRequests() + + c.log.V(logf.DebugLevel).Info("new certificate signing request controller registered", + "type", c.signerType) + + return c.queue, mustSync, nil +} + +func (c *Controller) ProcessItem(ctx context.Context, key string) error { + log := logf.FromContext(ctx) + dbg := log.V(logf.DebugLevel) + + _, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.Error(err, "invalid resource key") + return nil + } + + csr, err := c.csrLister.Get(name) + if apierrors.IsNotFound(err) { + dbg.Info("certificate request in work queue no longer exists", "error", err.Error()) + return nil + } + + if err != nil { + return err + } + + ctx = logf.NewContext(ctx, logf.WithResource(log, csr)) + return c.Sync(ctx, csr) +} diff --git a/pkg/controller/certificatesigningrequests/controller_test.go b/pkg/controller/certificatesigningrequests/controller_test.go new file mode 100644 index 000000000..b4f772b9f --- /dev/null +++ b/pkg/controller/certificatesigningrequests/controller_test.go @@ -0,0 +1,674 @@ +/* +Copyright 2021 The cert-manager 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 certificatesigningrequests + +import ( + "context" + "errors" + "testing" + "time" + + authzv1 "k8s.io/api/authorization/v1" + certificatesv1 "k8s.io/api/certificates/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + coretesting "k8s.io/client-go/testing" + fakeclock "k8s.io/utils/clock/testing" + + apiutil "github.com/jetstack/cert-manager/pkg/api/util" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake" + "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util" + testpkg "github.com/jetstack/cert-manager/pkg/controller/test" + "github.com/jetstack/cert-manager/test/unit/gen" +) + +func TestController(t *testing.T) { + fixedClockStart := time.Now() + fixedClock := fakeclock.NewFakeClock(fixedClockStart) + metaFixedClockStart := metav1.NewTime(fixedClockStart) + + signerExpectNoCall := func(t *testing.T) Signer { + return &fake.Signer{ + FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error { + t.Fatal("unexpected sign call") + return nil + }, + } + } + + sarReactionExpectNoCall := func(t *testing.T) coretesting.ReactionFunc { + return func(_ coretesting.Action) (bool, runtime.Object, error) { + t.Fatal("unexpected call") + return true, nil, nil + } + } + sarReactionAllow := func(t *testing.T) coretesting.ReactionFunc { + return func(_ coretesting.Action) (bool, runtime.Object, error) { + return true, &authzv1.SubjectAccessReview{ + Status: authzv1.SubjectAccessReviewStatus{ + Allowed: true, + }, + }, nil + } + } + + tests := map[string]struct { + // key that should be passed to ProcessItem. If not set, the + // 'namespace/name' of the 'CertificateSigningRequest' field will be used. + // If neither is set, the key will be "". + key string + + // CertificateSigningRequest to be synced for the test. If not set, the + // 'key' will be passed to ProcessItem instead. + existingCSR *certificatesv1.CertificateSigningRequest + + // If not nil, generic issuer object will be made available for the test + existingIssuer runtime.Object + + signerType string + + signerImpl func(t *testing.T) Signer + sarReaction func(t *testing.T) coretesting.ReactionFunc + wantSARCreation []*authzv1.SubjectAccessReview + + // wantEvent, if set, is an 'event string' that is expected to be fired. + wantEvent string + + // wantConditions is the expected set of conditions on the + // CertificateSigningRequest resource if an Update is made. + // If nil, no update is expected. + // If empty, an update to the empty set/nil is expected. + wantConditions []certificatesv1.CertificateSigningRequestCondition + + wantErr bool + }{ + "do nothing if an empty 'key' is used": { + signerType: apiutil.IssuerCA, + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if an invalid 'key' is used": { + key: "abc/def/ghi", + signerType: apiutil.IssuerCA, + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if a key references a CertificateSigningRequest that does not exist": { + key: "namespace/name", + signerType: apiutil.IssuerCA, + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if a key references a CertificateSigningRequest that has a malformed SignerName for cert-manager.io": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("malformed.signer.name/"), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "if CertificateSigningRequest references the cert-manager.io signer group but the type is not recognised, should ignore": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("foo.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if CertificateSigningRequest has a SignerName not for cert-manager.io": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.my-group.io/hello.world"), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if CertificateSigningRequest is marked as Failed": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateFailed, + Status: corev1.ConditionTrue, + Reason: "FailedReason", + Message: "Failed message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if CertificateSigningRequest is no yet approved": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "do nothing if CertificateSigningRequest already has a non empty Certificate present": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + gen.SetCertificateSigningRequestCertificate([]byte("non-empty-certificate")), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "if CertificateSigningRequest references an Issuer that does not exist, should fire an event that it can't be found": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + existingIssuer: nil, + wantEvent: "Warning IssuerNotFound Referenced Issuer hello/world not found", + }, + "if CertificateSigningRequest references an Issuer that does not yet have a type, should fire an event it doesn't have a type": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello")), + wantEvent: "Warning IssuerTypeMissing Referenced Issuer hello/world is missing type", + }, + "if CertificateSigningRequest references an Issuer which does not match the same signer type, should ignore": { + signerType: apiutil.IssuerSelfSigned, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + ), + }, + "do nothing if CertificateSigningRequest references a signer that is not 'issuers' or 'clusterissuers'": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("not-issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + }, + "if CertificateSigningRequest references a issuers signer but the SubjectAccessReview errors, should error": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestUsername("user-1"), + gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}), + gen.SetCertificateSigningRequestUID("uid-1"), + gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{ + "extra": []string{"1", "2"}, + }), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: func(t *testing.T) coretesting.ReactionFunc { + return func(_ coretesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("this is a simulated error") + } + }, + wantSARCreation: []*authzv1.SubjectAccessReview{ + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "world", + Version: "*", + }, + }, + }, + }, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + ), + wantErr: true, + }, + "if CertificateSigningRequest references a issuers signer but the requesting user does not have permissions, should update Failed": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestUsername("user-1"), + gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}), + gen.SetCertificateSigningRequestUID("uid-1"), + gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{ + "extra": []string{"1", "2"}, + }), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: func(t *testing.T) coretesting.ReactionFunc { + return func(_ coretesting.Action) (bool, runtime.Object, error) { + return true, &authzv1.SubjectAccessReview{ + Status: authzv1.SubjectAccessReviewStatus{ + Allowed: false, + }, + }, nil + } + }, + wantSARCreation: []*authzv1.SubjectAccessReview{ + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "world", + Version: "*", + }, + }, + }, + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "*", + Version: "*", + }, + }, + }, + }, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + ), + wantEvent: "Warning DeniedReference Requester may not reference Namespaced Issuer hello/world", + wantConditions: []certificatesv1.CertificateSigningRequestCondition{ + { + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }, + { + Type: certificatesv1.CertificateFailed, + Status: corev1.ConditionTrue, + Reason: "DeniedReference", + Message: "Requester may not reference Namespaced Issuer hello/world", + LastTransitionTime: metaFixedClockStart, + LastUpdateTime: metaFixedClockStart, + }, + }, + }, + "if CertificateSigningRequest references a clusterissuers signer but the signer name contains a namespace, should update with failed": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("clusterissuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionExpectNoCall, + existingIssuer: gen.ClusterIssuer("world", + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + ), + wantEvent: "Warning BadSignerName Signer clusterissuers may not be referenced with namespace (hello)", + wantConditions: []certificatesv1.CertificateSigningRequestCondition{ + { + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }, + { + Type: certificatesv1.CertificateFailed, + Status: corev1.ConditionTrue, + Reason: "BadSignerName", + Message: "Signer clusterissuers may not be referenced with namespace (hello)", + LastTransitionTime: metaFixedClockStart, + LastUpdateTime: metaFixedClockStart, + }, + }, + }, + "if CertificateSigningRequest references a issuers signer but the Issuer is not ready, fire event not Ready": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestUsername("user-1"), + gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}), + gen.SetCertificateSigningRequestUID("uid-1"), + gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{ + "extra": []string{"1", "2"}, + }), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: signerExpectNoCall, + sarReaction: sarReactionAllow, + wantSARCreation: []*authzv1.SubjectAccessReview{ + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "world", + Version: "*", + }, + }, + }, + }, + wantEvent: "Warning IssuerNotReady Referenced Issuer hello/world does not have a Ready status condition", + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + ), + }, + "if CertificateSigningRequest called invoked sign but it errors, should return error": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestUsername("user-1"), + gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}), + gen.SetCertificateSigningRequestUID("uid-1"), + gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{ + "extra": []string{"1", "2"}, + }), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: func(t *testing.T) Signer { + return &fake.Signer{ + FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error { + return errors.New("this is a simulated error") + }, + } + }, + sarReaction: sarReactionAllow, + wantSARCreation: []*authzv1.SubjectAccessReview{ + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "world", + Version: "*", + }, + }, + }, + }, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + gen.AddIssuerCondition(cmapi.IssuerCondition{ + Type: cmapi.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + Reason: "IssuerReady", + Message: "Issuer ready message", + }), + ), + wantErr: true, + }, + "if CertificateSigningRequest called invoked sign and doesn't error, should return no error": { + signerType: apiutil.IssuerCA, + existingCSR: gen.CertificateSigningRequest("csr-1", + gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"), + gen.SetCertificateSigningRequestUsername("user-1"), + gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}), + gen.SetCertificateSigningRequestUID("uid-1"), + gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{ + "extra": []string{"1", "2"}, + }), + gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "ApprovedReason", + Message: "Approved message", + }), + ), + signerImpl: func(t *testing.T) Signer { + return &fake.Signer{ + FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error { + return nil + }, + } + }, + sarReaction: sarReactionAllow, + wantSARCreation: []*authzv1.SubjectAccessReview{ + { + Spec: authzv1.SubjectAccessReviewSpec{ + User: "user-1", + Groups: []string{"group-1", "group-2"}, + Extra: map[string]authzv1.ExtraValue{ + "extra": []string{"1", "2"}, + }, + UID: "uid-1", + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: "cert-manager.io", + Resource: "signers", + Verb: "reference", + Namespace: "hello", + Name: "world", + Version: "*", + }, + }, + }, + }, + existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"), + gen.SetIssuerCA(cmapi.CAIssuer{ + SecretName: "tls", + }), + gen.AddIssuerCondition(cmapi.IssuerCondition{ + Type: cmapi.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + Reason: "IssuerReady", + Message: "Issuer ready message", + }), + ), + wantErr: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + util.Clock = fixedClock + builder := &testpkg.Builder{ + T: t, + Clock: fixedClock, + } + if test.existingIssuer != nil { + builder.CertManagerObjects = append(builder.CertManagerObjects, test.existingIssuer) + } + if test.existingCSR != nil { + builder.KubeObjects = append(builder.KubeObjects, test.existingCSR) + } + + for i := range test.wantSARCreation { + builder.ExpectedActions = append(builder.ExpectedActions, + testpkg.NewAction(coretesting.NewCreateAction( + authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"), + "", + test.wantSARCreation[i], + )), + ) + } + + builder.Init() + + builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) { + if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") { + return false, nil, nil + } + return test.sarReaction(t)(action) + }) + + controller := New(test.signerType, test.signerImpl(t)) + _, _, err := controller.Register(builder.Context) + if err != nil { + t.Fatal(err) + } + + if test.wantConditions != nil { + if test.existingCSR == nil { + t.Fatal("cannot expect an Update operation if test.existingCSR is nil") + } + expectedCSR := test.existingCSR.DeepCopy() + expectedCSR.Status.Conditions = test.wantConditions + builder.ExpectedActions = append(builder.ExpectedActions, + testpkg.NewAction(coretesting.NewUpdateSubresourceAction( + certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"), + "status", + "", + expectedCSR, + )), + ) + } + if test.wantEvent != "" { + builder.ExpectedEvents = []string{test.wantEvent} + } + + builder.Start() + defer builder.Stop() + + key := test.key + if key == "" && test.existingCSR != nil { + key, err = controllerpkg.KeyFunc(test.existingCSR) + if err != nil { + t.Fatal(err) + } + } + + gotErr := controller.ProcessItem(context.Background(), key) + if test.wantErr != (gotErr != nil) { + t.Errorf("got unexpected error, exp=%t got=%v", + test.wantErr, gotErr) + } + + builder.CheckAndFinish() + }) + } +} diff --git a/pkg/controller/certificatesigningrequests/fake/BUILD.bazel b/pkg/controller/certificatesigningrequests/fake/BUILD.bazel new file mode 100644 index 000000000..56f52abcf --- /dev/null +++ b/pkg/controller/certificatesigningrequests/fake/BUILD.bazel @@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["fake.go"], + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/certmanager/v1:go_default_library", + "@io_k8s_api//certificates/v1: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/pkg/controller/certificatesigningrequests/fake/fake.go b/pkg/controller/certificatesigningrequests/fake/fake.go new file mode 100644 index 000000000..acf524ed0 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/fake/fake.go @@ -0,0 +1,33 @@ +/* +Copyright 2021 The cert-manager 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 fake + +import ( + "context" + + certificatesv1 "k8s.io/api/certificates/v1" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" +) + +type Signer struct { + FakeSign func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error +} + +func (s *Signer) Sign(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerObj cmapi.GenericIssuer) error { + return s.FakeSign(ctx, csr, issuerObj) +} diff --git a/pkg/controller/certificatesigningrequests/sync.go b/pkg/controller/certificatesigningrequests/sync.go new file mode 100644 index 000000000..6d248b7cc --- /dev/null +++ b/pkg/controller/certificatesigningrequests/sync.go @@ -0,0 +1,197 @@ +/* +Copyright 2021 The cert-manager 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 certificatesigningrequests + +import ( + "context" + "fmt" + + authzv1 "k8s.io/api/authorization/v1" + certificatesv1 "k8s.io/api/certificates/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiutil "github.com/jetstack/cert-manager/pkg/api/util" + "github.com/jetstack/cert-manager/pkg/apis/certmanager" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util" + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +func (c *Controller) Sync(ctx context.Context, csr *certificatesv1.CertificateSigningRequest) error { + log := logf.WithResource(logf.FromContext(ctx), csr).WithValues("signerName", csr.Spec.SignerName) + dbg := log.V(logf.DebugLevel) + + ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName) + if !ok { + dbg.Info("certificate signing request has malformed signer name,", "signerName", csr.Spec.SignerName) + return nil + } + + if ref.Group != certmanager.GroupName { + dbg.Info("certificate signing request signerName group does not match 'cert-manager.io' group so skipping processing") + return nil + } + + if util.CertificateSigningRequestIsFailed(csr) { + dbg.Info("certificate signing request has failed so skipping processing") + return nil + } + if !util.CertificateSigningRequestIsApproved(csr) { + dbg.Info("certificate signing request is not approved so skipping processing") + return nil + } + + if len(csr.Status.Certificate) > 0 { + dbg.Info("certificate field is already set in status so skipping processing") + return nil + } + + var kind string + switch ref.Type { + case "issuers": + kind = cmapi.IssuerKind + break + + case "clusterissuers": + kind = cmapi.ClusterIssuerKind + break + + default: + dbg.Info("certificate signing request signerName type does not match 'issuers' or 'clusterissuers' so skipping processing") + return nil + } + + issuerObj, err := c.helper.GetGenericIssuer(cmmeta.ObjectReference{ + Name: ref.Name, + Kind: kind, + Group: ref.Group, + }, ref.Namespace) + if apierrors.IsNotFound(err) { + c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotFound", "Referenced %s %s/%s not found", kind, ref.Namespace, ref.Name) + return nil + } + + if err != nil { + log.Error(err, "failed to get issuer") + return err + } + + log = logf.WithRelatedResource(log, issuerObj) + dbg.Info("ensuring issuer type matches this controller") + + signerType, err := apiutil.NameForIssuer(issuerObj) + if err != nil { + c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerTypeMissing", "Referenced %s %s/%s is missing type", kind, ref.Namespace, ref.Name) + return nil + } + + // This CertificateSigningRequest is not meant for us, ignore + if signerType != c.signerType { + dbg.WithValues(logf.RelatedResourceKindKey, signerType).Info("signer reference type does not match controller resource kind, ignoring") + return nil + } + + switch kind { + case cmapi.IssuerKind: + ok, err := c.userCanReferenceSigner(ctx, csr, ref.Namespace, ref.Name) + if err != nil { + return err + } + if !ok { + message := fmt.Sprintf("Requester may not reference Namespaced Issuer %s/%s", ref.Namespace, ref.Name) + c.recorder.Event(csr, corev1.EventTypeWarning, "DeniedReference", message) + util.CertificateSigningRequestSetFailed(csr, "DeniedReference", message) + if _, err := c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{}); err != nil { + return err + } + + return nil + } + case cmapi.ClusterIssuerKind: + // Namespace not valid for a clusterissuer + if len(ref.Namespace) > 0 { + message := fmt.Sprintf("Signer clusterissuers may not be referenced with namespace (%s)", ref.Namespace) + c.recorder.Event(csr, corev1.EventTypeWarning, "BadSignerName", message) + util.CertificateSigningRequestSetFailed(csr, "BadSignerName", message) + if _, err := c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{}); err != nil { + return err + } + return nil + } + } + + // check ready condition + if !apiutil.IssuerHasCondition(issuerObj, cmapi.IssuerCondition{ + Type: cmapi.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) { + c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotReady", "Referenced %s %s/%s does not have a Ready status condition", + kind, issuerObj.GetNamespace(), issuerObj.GetName()) + return nil + } + + dbg.Info("invoking sign function as existing certificate does not exist") + + return c.signer.Sign(ctx, csr, issuerObj) +} + +// userCanReferenceSigner will return true if the CSR requester has a bound +// role that allows them to reference a given Namespaced signer. The user must +// have the permissions: +// group: cert-manager.io +// resource: signers +// verb: reference +// namespace: +// name: +func (c *Controller) userCanReferenceSigner(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerNamespace, issuerName string) (bool, error) { + extra := make(map[string]authzv1.ExtraValue) + for k, v := range csr.Spec.Extra { + extra[k] = authzv1.ExtraValue(v) + } + + for _, name := range []string{issuerName, "*"} { + resp, err := c.sarClient.Create(ctx, &authzv1.SubjectAccessReview{ + Spec: authzv1.SubjectAccessReviewSpec{ + User: csr.Spec.Username, + Groups: csr.Spec.Groups, + Extra: extra, + UID: csr.Spec.UID, + + ResourceAttributes: &authzv1.ResourceAttributes{ + Group: certmanager.GroupName, + Resource: "signers", + Verb: "reference", + Namespace: issuerNamespace, + Name: name, + Version: "*", + }, + }, + }, metav1.CreateOptions{}) + if err != nil { + return false, err + } + + if resp.Status.Allowed { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/controller/certificatesigningrequests/util/BUILD.bazel b/pkg/controller/certificatesigningrequests/util/BUILD.bazel new file mode 100644 index 000000000..f520c30d0 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/util/BUILD.bazel @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "conditions.go", + "signername.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util", + visibility = ["//visibility:public"], + deps = [ + "//pkg/logs:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", + "@io_k8s_api//core/v1:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_utils//clock:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["signername_test.go"], + embed = [":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/pkg/controller/certificatesigningrequests/util/conditions.go b/pkg/controller/certificatesigningrequests/util/conditions.go new file mode 100644 index 000000000..0ed5cfb40 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/util/conditions.go @@ -0,0 +1,65 @@ +/* +Copyright 2021 The cert-manager 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 util + +import ( + certificatesv1 "k8s.io/api/certificates/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/clock" + + logf "github.com/jetstack/cert-manager/pkg/logs" +) + +// Clock is defined as a package var so it can be stubbed out during tests. +var Clock clock.Clock = clock.RealClock{} + +func CertificateSigningRequestIsApproved(csr *certificatesv1.CertificateSigningRequest) bool { + for _, cond := range csr.Status.Conditions { + if cond.Type == certificatesv1.CertificateApproved { + return true + } + } + return false +} + +func CertificateSigningRequestIsFailed(csr *certificatesv1.CertificateSigningRequest) bool { + for _, cond := range csr.Status.Conditions { + if cond.Type == certificatesv1.CertificateFailed { + return true + } + } + return false +} + +func CertificateSigningRequestSetFailed(csr *certificatesv1.CertificateSigningRequest, reason, message string) { + nowTime := metav1.NewTime(Clock.Now()) + + // Since we only ever set this condition once (enforced by the API), we + // needn't need to check whether the condition is already set. + csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.CertificateFailed, + Status: corev1.ConditionTrue, + Reason: reason, + Message: message, + LastTransitionTime: nowTime, + LastUpdateTime: nowTime, + }) + + logf.V(logf.InfoLevel).Infof("Setting lastTransitionTime for CertificateSigningRequest %s/%s condition Failed to %v", + csr.Namespace, csr.Name, nowTime.Time) +} diff --git a/pkg/controller/certificatesigningrequests/util/signername.go b/pkg/controller/certificatesigningrequests/util/signername.go new file mode 100644 index 000000000..a7685ad06 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/util/signername.go @@ -0,0 +1,63 @@ +/* +Copyright 2021 The cert-manager 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 util + +import ( + "strings" +) + +type SignerIssuerRef struct { + Namespace, Name string + Type, Group string +} + +// SignerIssuerRefFromSignerName will return a SignerIssuerRef from a +// CertificateSigningRequests.SignerName +func SignerIssuerRefFromSignerName(name string) (SignerIssuerRef, bool) { + split := strings.Split(name, "/") + if len(split) != 2 { + return SignerIssuerRef{}, false + } + + signerTypeSplit := strings.SplitN(split[0], ".", 2) + signerNameSplit := strings.Split(split[1], ".") + + if len(signerTypeSplit) < 2 || signerNameSplit[0] == "" { + return SignerIssuerRef{}, false + } + + switch len(signerNameSplit) { + case 1: + return SignerIssuerRef{ + Namespace: "", + Name: signerNameSplit[0], + Type: signerTypeSplit[0], + Group: signerTypeSplit[1], + }, true + + case 2: + return SignerIssuerRef{ + Namespace: signerNameSplit[0], + Name: signerNameSplit[1], + Type: signerTypeSplit[0], + Group: signerTypeSplit[1], + }, true + + default: + return SignerIssuerRef{}, false + } +} diff --git a/pkg/controller/certificatesigningrequests/util/signername_test.go b/pkg/controller/certificatesigningrequests/util/signername_test.go new file mode 100644 index 000000000..ba02fb867 --- /dev/null +++ b/pkg/controller/certificatesigningrequests/util/signername_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2021 The cert-manager 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 util + +import ( + "reflect" + "testing" +) + +func TestIssuerRefFromSignerName(t *testing.T) { + tests := map[string]struct { + inpName string + expSignerIssuerRef SignerIssuerRef + expOK bool + }{ + "an empty name should return false": { + inpName: "", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference without a name should return false": { + inpName: "foo.bar", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference with a '/' but no name should return false": { + inpName: "foo.bar/", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference with no host should return false": { + inpName: "/foo.bar", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference with only one domain should return false": { + inpName: "abc/hello-world", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference with too many names should return false": { + inpName: "foo.bar/hello.world.123", + expSignerIssuerRef: SignerIssuerRef{}, + expOK: false, + }, + "a reference with 2 domains and 2 names should return namespaced issuer": { + inpName: "foo.bar/hello.world", + expSignerIssuerRef: SignerIssuerRef{ + Namespace: "hello", + Name: "world", + Type: "foo", + Group: "bar", + }, + expOK: true, + }, + "a reference with 4 domains and 2 names should return namespaced issuer": { + inpName: "foo.bar.abc.dbc/hello.world", + expSignerIssuerRef: SignerIssuerRef{ + Namespace: "hello", + Name: "world", + Type: "foo", + Group: "bar.abc.dbc", + }, + expOK: true, + }, + "a reference with 2 domains and one name should return cluster issuer": { + inpName: "foo.bar/hello-world", + expSignerIssuerRef: SignerIssuerRef{ + Namespace: "", + Name: "hello-world", + Type: "foo", + Group: "bar", + }, + expOK: true, + }, + "a reference with 4 domains and 1 name should return cluster issuer": { + inpName: "foo.bar.abc.dbc/hello-world", + expSignerIssuerRef: SignerIssuerRef{ + Namespace: "", + Name: "hello-world", + Type: "foo", + Group: "bar.abc.dbc", + }, + expOK: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ref, ok := SignerIssuerRefFromSignerName(test.inpName) + if ok != test.expOK { + t.Errorf("unexpected ok, exp=%t got=%t", + test.expOK, ok) + } + + if !reflect.DeepEqual(ref, test.expSignerIssuerRef) { + t.Errorf("unexpected SignerIssuerRef, exp=%v got=%v", + test.expSignerIssuerRef, ref) + } + }) + } +} diff --git a/pkg/util/pki/BUILD.bazel b/pkg/util/pki/BUILD.bazel index 9007275a6..2ebbf0830 100644 --- a/pkg/util/pki/BUILD.bazel +++ b/pkg/util/pki/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "csr.go", "generate.go", "keyusage.go", + "kube.go", "parse.go", ], importpath = "github.com/jetstack/cert-manager/pkg/util/pki", @@ -13,7 +14,9 @@ go_library( deps = [ "//pkg/api/util:go_default_library", "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/apis/experimental/v1alpha1:go_default_library", "//pkg/util/errors:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", ], ) diff --git a/test/unit/gen/BUILD.bazel b/test/unit/gen/BUILD.bazel index 2f8c6d9bd..2943a94d3 100644 --- a/test/unit/gen/BUILD.bazel +++ b/test/unit/gen/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "certificate.go", "certificaterequest.go", + "certificatesigningrequest.go", "challenge.go", "conditions.go", "csr.go", @@ -19,8 +20,10 @@ go_library( deps = [ "//pkg/apis/acme/v1:go_default_library", "//pkg/apis/certmanager/v1:go_default_library", + "//pkg/apis/experimental/v1alpha1:go_default_library", "//pkg/apis/meta/v1:go_default_library", "//pkg/util/pki:go_default_library", + "@io_k8s_api//certificates/v1:go_default_library", "@io_k8s_api//core/v1:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_apimachinery//pkg/types:go_default_library", diff --git a/test/unit/gen/certificatesigningrequest.go b/test/unit/gen/certificatesigningrequest.go new file mode 100644 index 000000000..e8bed4c97 --- /dev/null +++ b/test/unit/gen/certificatesigningrequest.go @@ -0,0 +1,142 @@ +/* +Copyright 2021 The cert-manager 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 gen + +import ( + "encoding/base64" + "strconv" + + certificatesv1 "k8s.io/api/certificates/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1" +) + +type CertificateSigningRequestModifier func(*certificatesv1.CertificateSigningRequest) + +func CertificateSigningRequest(name string, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest { + c := &certificatesv1.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Annotations: make(map[string]string), + Labels: make(map[string]string), + }, + } + for _, mod := range mods { + mod(c) + } + return c +} + +func CertificateSigningRequestFrom(cr *certificatesv1.CertificateSigningRequest, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest { + cr = cr.DeepCopy() + for _, mod := range mods { + mod(cr) + } + return cr +} + +func SetCertificateSigningRequestIsCA(isCA bool) CertificateSigningRequestModifier { + return AddCertificateSigningRequestAnnotations(map[string]string{ + experimentalapi.CertificateSigningRequestIsCAAnnotationKey: strconv.FormatBool(isCA), + }) +} + +func SetCertificateSigningRequestRequest(request []byte) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.Request = request + } +} + +func AddCertificateSigningRequestAnnotations(annotations map[string]string) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + // Make sure to do a merge here with new annotations overriding. + annotationsNew := csr.GetAnnotations() + if annotationsNew == nil { + annotationsNew = make(map[string]string) + } + for k, v := range annotations { + annotationsNew[k] = v + } + csr.SetAnnotations(annotationsNew) + } +} + +func SetCertificateSigningRequestSignerName(signerName string) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.SignerName = signerName + } +} + +func SetCertificateSigningRequestDuration(duration string) CertificateSigningRequestModifier { + return AddCertificateSigningRequestAnnotations(map[string]string{ + experimentalapi.CertificateSigningRequestDurationAnnotationKey: duration, + }) +} + +func SetCertificateSigningRequestCertificate(cert []byte) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Status.Certificate = cert + } +} + +func SetCertificateSigningRequestCA(ca []byte) CertificateSigningRequestModifier { + return AddCertificateSigningRequestAnnotations(map[string]string{ + experimentalapi.CertificateSigningRequestCAAnnotationKey: base64.StdEncoding.EncodeToString(ca), + }) +} + +func SetCertificateSigningRequestStatusCondition(c certificatesv1.CertificateSigningRequestCondition) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + if len(csr.Status.Conditions) == 0 { + csr.Status.Conditions = []certificatesv1.CertificateSigningRequestCondition{c} + return + } + + for i, existingC := range csr.Status.Conditions { + if existingC.Type == c.Type { + csr.Status.Conditions[i] = c + return + } + } + csr.Status.Conditions = append(csr.Status.Conditions, c) + } +} + +func SetCertificateSigningRequestUsername(username string) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.Username = username + } +} + +func SetCertificateSigningRequestGroups(groups []string) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.Groups = groups + } +} + +func SetCertificateSigningRequestUID(uid string) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.UID = uid + } +} + +func SetCertificateSigningRequestExtra(extra map[string]certificatesv1.ExtraValue) CertificateSigningRequestModifier { + return func(csr *certificatesv1.CertificateSigningRequest) { + csr.Spec.Extra = extra + } +}