diff --git a/apis/offline/v1alpha1/addofflinelicense_type.go b/apis/offline/v1alpha1/addofflinelicense_type.go index ca8fdf68a1..d32818dc33 100644 --- a/apis/offline/v1alpha1/addofflinelicense_type.go +++ b/apis/offline/v1alpha1/addofflinelicense_type.go @@ -28,6 +28,7 @@ const ( ) // +genclient +// +genclient:nonNamespaced // +genclient:onlyVerbs=create // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -40,7 +41,8 @@ type AddOfflineLicense struct { } type AddOfflineLicenseRequest struct { - License string `json:"license"` + Namespace string `json:"namespace"` + License string `json:"license"` } type AddOfflineLicenseResponse struct { diff --git a/apis/offline/v1alpha1/openapi_generated.go b/apis/offline/v1alpha1/openapi_generated.go index 2d86cf91f9..55738caaa0 100644 --- a/apis/offline/v1alpha1/openapi_generated.go +++ b/apis/offline/v1alpha1/openapi_generated.go @@ -18412,6 +18412,13 @@ func schema_ui_server_apis_offline_v1alpha1_AddOfflineLicenseRequest(ref common. SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "namespace": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "license": { SchemaProps: spec.SchemaProps{ Default: "", @@ -18420,7 +18427,7 @@ func schema_ui_server_apis_offline_v1alpha1_AddOfflineLicenseRequest(ref common. }, }, }, - Required: []string{"license"}, + Required: []string{"namespace", "license"}, }, }, } diff --git a/artifacts/addlicense.yaml b/artifacts/addlicense.yaml new file mode 100644 index 0000000000..6723ddeff9 --- /dev/null +++ b/artifacts/addlicense.yaml @@ -0,0 +1,32 @@ +apiVersion: offline.licenses.appscode.com/v1alpha1 +kind: AddOfflineLicense + +request: + namespace: kubeops + license: | + -----BEGIN CERTIFICATE----- + MIIEUDCCAzigAwIBAgIIc/mtiBJRhtUwDQYJKoZIhvcNAQELBQAwJTEWMBQGA1UE + ChMNQXBwc0NvZGUgSW5jLjELMAkGA1UEAxMCY2EwHhcNMjQwNTE1MTA1NDQwWhcN + MjQwNjE0MTA1NDQwWjCCARgxDzANBgNVBAYTBmt1YmVkYjETMBEGA1UECBMKZW50 + ZXJwcmlzZTGBpDAXBgNVBAoTEGt1YmVkYi1jb21tdW5pdHkwFwYDVQQKExBrdWJl + ZGItZXh0LXN0YXNoMBgGA1UEChMRa3ViZWRiLWF1dG9zY2FsZXIwGAYDVQQKExFr + dWJlZGItZW50ZXJwcmlzZTAcBgNVBAoTFXBhbm9wdGljb24tZW50ZXJwcmlzZTAe + BgNVBAoTF2t1YmVkYi1tb25pdG9yaW5nLWFnZW50MRowGAYDVQQLExFrdWJlZGIt + ZW50ZXJwcmlzZTEtMCsGA1UEAxMkZTg2NTk1OWEtMjk2ZS00OWVhLWI5NjktNGZh + MmRkMGU2NTBmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApBsr5C40 + 3IMzdy/EA1pvYO2RCaf9xmwfcI5aofWbox821UgTzwA1OqwXByh7ANQNPAcvPQTn + 2NMBoqdjdVQ/PHuHP8vTVvcXjS07g061HGW4LuEXYwoxd8ANBm8wKSdaHcH2KqBm + PdqNAST2r5gPj+vwdbr3KQkMNA6xJK9fNe/Ho1UoWc7/N1ex+HQdV+iBT+wYDIwm + VxPVy5mUdxMx+mJMLBehgVs/tdDHqc619+vXzl9hKXtKczzzcpEM9DOjjkfHCdMa + f9Y+7XeRlZ3659DQowwVVAYXi6oYv4Dn7ahXpPR5KBZMQu290pUgvTA+4AJjFjIF + AkIxiQndHbtPMQIDAQABo4GOMIGLMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAK + BggrBgEFBQcDAjBkBgNVHREEXTBbgiRlODY1OTU5YS0yOTZlLTQ5ZWEtYjk2OS00 + ZmEyZGQwZTY1MGaBH1RhbWFsIFNhaGEgPHRhbWFsQGFwcHNjb2RlLmNvbT6BEnRh + bWFsQGFwcHNjb2RlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAJ+vBtrzfDhtndWHn + kcJ9wgKfDWtW1kfhTPvIPO+YFV1vHHGyxuS6TwMNCsiXwAMu5Qao+pWCUrV0CfCC + p405HtO9aHfSPEz2YcoqanIevvaUzDP6DeUW6BCE4LwaPOdCtiUdLx57F7yCu7K4 + SQLZv9ufIdyZ9oDz5hvNuPWtPorB0/rgHWPxFmOJfVdCxp1gKqN/QJcwicC75P7U + QcwYQFpsGkSYZijKrB1fGmULf+p3ig6+JKXdPw27K/rnDaP8emjVYwoUibAvL8O1 + u0FfA5lbD0X/VLBPz+Czvsunn6ESZCEba7TQUEv6S4rfuRdKFFVj4ljza+W75DH7 + OKfieg== + -----END CERTIFICATE----- diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 3d70398589..c73732c467 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -30,6 +30,8 @@ import ( costapi "kubeops.dev/ui-server/apis/cost/v1alpha1" identityinstall "kubeops.dev/ui-server/apis/identity/install" identityv1alpha1 "kubeops.dev/ui-server/apis/identity/v1alpha1" + licenseinstall "kubeops.dev/ui-server/apis/offline/install" + licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1" policyinstall "kubeops.dev/ui-server/apis/policy/install" policyapi "kubeops.dev/ui-server/apis/policy/v1alpha1" projectquotacontroller "kubeops.dev/ui-server/pkg/controllers/projectquota" @@ -61,6 +63,8 @@ import ( "kubeops.dev/ui-server/pkg/registry/meta/resourcetabledefinition" "kubeops.dev/ui-server/pkg/registry/meta/usermenu" "kubeops.dev/ui-server/pkg/registry/meta/vendormenu" + "kubeops.dev/ui-server/pkg/registry/offline/addofflinelicense" + "kubeops.dev/ui-server/pkg/registry/offline/offlinelicense" policystorage "kubeops.dev/ui-server/pkg/registry/policy/reports" imagestorage "kubeops.dev/ui-server/pkg/registry/scanner/image" reportstorage "kubeops.dev/ui-server/pkg/registry/scanner/reports" @@ -125,6 +129,7 @@ func init() { rscoreinstall.Install(Scheme) mgmtinstall.Install(Scheme) crdinstall.Install(Scheme) + licenseinstall.Install(Scheme) utilruntime.Must(scannerscheme.AddToScheme(Scheme)) utilruntime.Must(chartsapi.AddToScheme(Scheme)) utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) @@ -310,6 +315,18 @@ func (c completedConfig) New(ctx context.Context) (*UIServer, error) { return nil, err } } + { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(licenseapi.GroupName, Scheme, metav1.ParameterCodec, Codecs) + + v1alpha1storage := map[string]rest.Storage{} + v1alpha1storage[licenseapi.ResourceOfflineLicenses] = offlinelicense.NewStorage(ctrlClient) + v1alpha1storage[licenseapi.ResourceAddOfflineLicenses] = addofflinelicense.NewStorage(ctrlClient, cid, rbacAuthorizer) + apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage + + if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil { + return nil, err + } + } { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(auditor.GroupName, Scheme, metav1.ParameterCodec, Codecs) diff --git a/pkg/cmds/server/start.go b/pkg/cmds/server/start.go index 83dac125f5..73abeb520f 100644 --- a/pkg/cmds/server/start.go +++ b/pkg/cmds/server/start.go @@ -27,6 +27,7 @@ import ( reportsapi "kubeops.dev/scanner/apis/reports/v1alpha1" costapi "kubeops.dev/ui-server/apis/cost/v1alpha1" identityv1alpha1 "kubeops.dev/ui-server/apis/identity/v1alpha1" + licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1" policyapi "kubeops.dev/ui-server/apis/policy/v1alpha1" "kubeops.dev/ui-server/pkg/apiserver" featurecontroller "kubeops.dev/ui-server/pkg/controllers/feature" @@ -159,6 +160,10 @@ func (o *UIServerOptions) Config() (*apiserver.Config, error) { fmt.Sprintf("/apis/%s/%s", rsapi.SchemeGroupVersion, rsapi.ResourceResourceTableDefinitions), fmt.Sprintf("/apis/%s/%s", rscoreapi.SchemeGroupVersion, rscoreapi.ResourceProjects), + + fmt.Sprintf("/apis/%s", licenseapi.SchemeGroupVersion), + fmt.Sprintf("/apis/%s/%s", licenseapi.SchemeGroupVersion, licenseapi.ResourceAddOfflineLicenses), + fmt.Sprintf("/apis/%s/%s", licenseapi.SchemeGroupVersion, licenseapi.ResourceOfflineLicenses), } serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig( diff --git a/pkg/registry/offline/addofflinelicense/storage.go b/pkg/registry/offline/addofflinelicense/storage.go new file mode 100644 index 0000000000..ec9a2097a1 --- /dev/null +++ b/pkg/registry/offline/addofflinelicense/storage.go @@ -0,0 +1,216 @@ +/* +Copyright AppsCode Inc. and Contributors. + +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 addofflinelicense + +import ( + "context" + "errors" + "fmt" + "strings" + + licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1" + + core "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/authorization/authorizer" + apirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/util/cert" + cg "kmodules.xyz/client-go/client" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + LicenseSecretName = "license-proxyserver-licenses" +) + +var secretGR = schema.GroupResource{ + Group: "", + Resource: "secrets", +} + +type Storage struct { + kc client.Client + clusterID string + a authorizer.Authorizer +} + +var ( + _ rest.GroupVersionKindProvider = &Storage{} + _ rest.Scoper = &Storage{} + _ rest.Storage = &Storage{} + _ rest.Creater = &Storage{} + _ rest.SingularNameProvider = &Storage{} +) + +func NewStorage(kc client.Client, clusterID string, a authorizer.Authorizer) *Storage { + return &Storage{ + kc: kc, + clusterID: clusterID, + a: a, + } +} + +func (r *Storage) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind { + return licenseapi.SchemeGroupVersion.WithKind(licenseapi.ResourceKindAddOfflineLicense) +} + +func (r *Storage) NamespaceScoped() bool { + return false +} + +func (r *Storage) GetSingularName() string { + return strings.ToLower(licenseapi.ResourceKindAddOfflineLicense) +} + +func (r *Storage) New() runtime.Object { + return &licenseapi.AddOfflineLicense{} +} + +func (r *Storage) Destroy() {} + +func (r *Storage) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { + in := obj.(*licenseapi.AddOfflineLicense) + if in.Request == nil { + return nil, apierrors.NewBadRequest("missing apirequest") + } + req := in.Request + + user, ok := apirequest.UserFrom(ctx) + if !ok { + return nil, apierrors.NewBadRequest("missing user info") + } + + if req.Namespace == "" { + return nil, apierrors.NewBadRequest("missing license secret namespace") + } + if req.License == "" { + return nil, apierrors.NewBadRequest("missing license info") + } + + licenseSecret := v1.Secret{} + err := r.kc.Get(ctx, types.NamespacedName{Name: LicenseSecretName, Namespace: req.Namespace}, &licenseSecret) + if err != nil && apierrors.IsNotFound(err) { + // check permission + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "create", + Namespace: req.Namespace, + APIGroup: secretGR.Group, + Resource: secretGR.Resource, + Name: LicenseSecretName, + ResourceRequest: true, + } + decision, why, err := r.a.Authorize(ctx, attrs) + if err != nil { + return nil, apierrors.NewInternalError(err) + } + if decision != authorizer.DecisionAllow { + return nil, apierrors.NewForbidden(secretGR, LicenseSecretName, errors.New(why)) + } + + productKey, err := getProductKey([]byte(req.License), r.clusterID) + if err != nil { + return nil, err + } + + licenseSecret = v1.Secret{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: LicenseSecretName, + Namespace: req.Namespace, + }, + Data: map[string][]byte{ + productKey: []byte(req.License), + }, + } + if err = r.kc.Create(ctx, &licenseSecret); err != nil { + return nil, err + } + + in.Response = &licenseapi.AddOfflineLicenseResponse{ + SecretKeyRef: &core.SecretKeySelector{ + LocalObjectReference: core.LocalObjectReference{ + Name: licenseSecret.Name, + }, + Key: productKey, + }, + } + return in, nil + } else if err != nil { + return nil, err + } + + // check permission + attrs := authorizer.AttributesRecord{ + User: user, + Verb: "patch", + Namespace: req.Namespace, + APIGroup: secretGR.Group, + Resource: secretGR.Resource, + Name: LicenseSecretName, + ResourceRequest: true, + } + decision, why, err := r.a.Authorize(ctx, attrs) + if err != nil { + return nil, apierrors.NewInternalError(err) + } + if decision != authorizer.DecisionAllow { + return nil, apierrors.NewForbidden(secretGR, LicenseSecretName, errors.New(why)) + } + + productKey, err := getProductKey([]byte(req.License), r.clusterID) + if err != nil { + return nil, err + } + licenseSecret.Data[productKey] = []byte(req.License) + + _, err = cg.CreateOrPatch(ctx, r.kc, &licenseSecret, func(obj client.Object, createOp bool) client.Object { + in := obj.(*v1.Secret) + in.Data = licenseSecret.Data + return in + }) + if err != nil { + return nil, err + } + + in.Response = &licenseapi.AddOfflineLicenseResponse{ + SecretKeyRef: &core.SecretKeySelector{ + LocalObjectReference: core.LocalObjectReference{ + Name: licenseSecret.Name, + }, + Key: productKey, + }, + } + return in, nil +} + +func getProductKey(lic []byte, clusterID string) (string, error) { + certs, err := cert.ParseCertsPEM(lic) + if err != nil { + return "", err + } + if certs[0].Subject.CommonName != clusterID { + return "", fmt.Errorf("license is for cluster %s, expecting %s", certs[0].Subject.CommonName, clusterID) + } + return certs[0].Subject.OrganizationalUnit[0], nil +} diff --git a/pkg/registry/offline/offlinelicense/storage.go b/pkg/registry/offline/offlinelicense/storage.go new file mode 100644 index 0000000000..31c770ff56 --- /dev/null +++ b/pkg/registry/offline/offlinelicense/storage.go @@ -0,0 +1,228 @@ +/* +Copyright AppsCode Inc. and Contributors. + +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 offlinelicense + +import ( + "context" + "strings" + + licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1" + "kubeops.dev/ui-server/pkg/registry/offline/addofflinelicense" + + "github.com/google/uuid" + verifier "go.bytebuilders.dev/license-verifier" + core "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + kerr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + apirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/util/cert" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Storage struct { + kc client.Client + convertor rest.TableConvertor +} + +var ( + _ rest.GroupVersionKindProvider = &Storage{} + _ rest.Scoper = &Storage{} + _ rest.Storage = &Storage{} + _ rest.Lister = &Storage{} + _ rest.SingularNameProvider = &Storage{} +) + +func NewStorage(kc client.Client) *Storage { + return &Storage{ + kc: kc, + convertor: rest.NewDefaultTableConvertor(schema.GroupResource{ + Group: licenseapi.GroupName, + Resource: licenseapi.ResourceOfflineLicenses, + }), + } +} + +func (r *Storage) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind { + return licenseapi.SchemeGroupVersion.WithKind(licenseapi.ResourceKindOfflineLicense) +} + +func (r *Storage) NamespaceScoped() bool { + return true +} + +func (r *Storage) GetSingularName() string { + return strings.ToLower(licenseapi.ResourceKindOfflineLicense) +} + +func (r *Storage) New() runtime.Object { + return &licenseapi.OfflineLicense{} +} + +func (r *Storage) Destroy() {} + +func (r *Storage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ns, ok := apirequest.NamespaceFrom(ctx) + if !ok { + return nil, apierrors.NewBadRequest("missing namespace") + } + + licenseSecret, err := getLicenseSecret(ctx, r.kc, ns) + if err != nil { + return &licenseapi.OfflineLicense{}, err + } + + for product, lic := range licenseSecret.Data { + if product == name { + certs, err := cert.ParseCertsPEM(lic) + if err != nil { + return nil, err + } + + license, err := verifier.ParseLicense(verifier.ParserOptions{ + ClusterUID: certs[0].Subject.CommonName, + CACert: certs[0], + License: lic, + }) + if err != nil && ignoreCertificateExpiredError(err) != nil { + return nil, err + } + + return &licenseapi.OfflineLicense{ + ObjectMeta: metav1.ObjectMeta{ + Name: license.PlanName, + Namespace: licenseSecret.Namespace, + CreationTimestamp: *license.NotBefore, + UID: types.UID(uuid.Must(uuid.NewUUID()).String()), + }, + Status: licenseapi.OfflineLicenseStatus{ + License: license, + SecretKeyRef: &core.SecretKeySelector{ + LocalObjectReference: core.LocalObjectReference{ + Name: licenseSecret.Name, + }, + Key: product, + }, + }, + }, nil + } + } + + return &licenseapi.OfflineLicense{}, err +} + +// Lister +func (r *Storage) NewList() runtime.Object { + return &licenseapi.OfflineLicenseList{} +} + +func (r *Storage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ns, ok := apirequest.NamespaceFrom(ctx) + if !ok { + return nil, apierrors.NewBadRequest("missing namespace") + } + + var licenses []licenseapi.OfflineLicense + var err error + + list, err := listLicenseSecrets(ctx, r.kc, ns) + for _, licenseSecret := range list { + for product, lic := range licenseSecret.Data { + certs, err := cert.ParseCertsPEM(lic) + if err != nil { + return nil, err + } + + license, err := verifier.ParseLicense(verifier.ParserOptions{ + ClusterUID: certs[0].Subject.CommonName, + CACert: certs[0], + License: lic, + }) + if err != nil && ignoreCertificateExpiredError(err) != nil { + return nil, err + } + + licenses = append(licenses, licenseapi.OfflineLicense{ + ObjectMeta: metav1.ObjectMeta{ + Name: license.PlanName, + Namespace: licenseSecret.Namespace, + CreationTimestamp: *license.NotBefore, + UID: types.UID(uuid.Must(uuid.NewUUID()).String()), + }, + Status: licenseapi.OfflineLicenseStatus{ + License: license, + SecretKeyRef: &core.SecretKeySelector{ + LocalObjectReference: core.LocalObjectReference{ + Name: licenseSecret.Name, + }, + Key: product, + }, + }, + }) + } + } + + result := licenseapi.OfflineLicenseList{ + TypeMeta: metav1.TypeMeta{}, + Items: licenses, + } + + return &result, err +} + +func (r *Storage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.convertor.ConvertToTable(ctx, object, tableOptions) +} + +func ignoreCertificateExpiredError(err error) error { + if strings.Contains(err.Error(), "x509: certificate has expired or is not yet valid") { + return nil + } + return err +} + +func getLicenseSecret(ctx context.Context, kc client.Client, ns string) (*core.Secret, error) { + var licenseSecret core.Secret + err := kc.Get(ctx, types.NamespacedName{Name: addofflinelicense.LicenseSecretName, Namespace: ns}, &licenseSecret) + if err != nil && kerr.IsNotFound(err) { + return &core.Secret{}, nil // never return nil + } else if err != nil { + return &core.Secret{}, err // never return nil + } + return &licenseSecret, nil +} + +func listLicenseSecrets(ctx context.Context, kc client.Client, ns string) ([]core.Secret, error) { + var list core.SecretList + err := kc.List(ctx, &list, client.InNamespace(ns)) + if err != nil { + return nil, err + } + + licenseSecrets := make([]core.Secret, 0, len(list.Items)) + for _, secret := range list.Items { + if secret.Name == addofflinelicense.LicenseSecretName { + licenseSecrets = append(licenseSecrets, secret) + } + } + return licenseSecrets, nil +} diff --git a/vendor/go.bytebuilders.dev/license-verifier/.gitignore b/vendor/go.bytebuilders.dev/license-verifier/.gitignore new file mode 100644 index 0000000000..8c65b9bc4e --- /dev/null +++ b/vendor/go.bytebuilders.dev/license-verifier/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +.idea/ +.vscode/ +.DS_Store diff --git a/vendor/go.bytebuilders.dev/license-verifier/Makefile b/vendor/go.bytebuilders.dev/license-verifier/Makefile new file mode 100644 index 0000000000..0c6cb92fbe --- /dev/null +++ b/vendor/go.bytebuilders.dev/license-verifier/Makefile @@ -0,0 +1,279 @@ +# Copyright AppsCode Inc. and Contributors +# +# 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. + +SHELL=/bin/bash -o pipefail + +GO_PKG := go.bytebuilders.dev +REPO := $(notdir $(shell pwd)) +BIN := license-verifier +COMPRESS ?= no + +# Produce CRDs that work back to Kubernetes 1.11 (no version conversion) +CRD_OPTIONS ?= "crd:maxDescLen=0,generateEmbeddedObjectMeta=true,allowDangerousTypes=true" +CODE_GENERATOR_IMAGE ?= ghcr.io/appscode/gengo:release-1.29 +API_GROUPS ?= licenses:v1alpha1 + +# Where to push the docker image. +REGISTRY ?= bytebuilders + +# This version-strategy uses git tags to set the version string +git_branch := $(shell git rev-parse --abbrev-ref HEAD) +git_tag := $(shell git describe --exact-match --abbrev=0 2>/dev/null || echo "") +commit_hash := $(shell git rev-parse --verify HEAD) +commit_timestamp := $(shell date --date="@$$(git show -s --format=%ct)" --utc +%FT%T) + +VERSION := $(shell git describe --tags --always --dirty) +version_strategy := commit_hash +ifdef git_tag + VERSION := $(git_tag) + version_strategy := tag +else + ifeq (,$(findstring $(git_branch),master HEAD)) + ifneq (,$(patsubst release-%,,$(git_branch))) + VERSION := $(git_branch) + version_strategy := branch + endif + endif +endif + +### +### These variables should not need tweaking. +### + +SRC_PKGS := apis info client # directories which hold app source excluding tests (not vendored) +SRC_DIRS := $(SRC_PKGS) *.go # directories which hold app source (not vendored) + +DOCKER_PLATFORMS := linux/amd64 linux/arm linux/arm64 +BIN_PLATFORMS := $(DOCKER_PLATFORMS) windows/amd64 darwin/amd64 + +# Used internally. Users should pass GOOS and/or GOARCH. +OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) +ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) + +BASEIMAGE_PROD ?= gcr.io/distroless/static-debian11 +BASEIMAGE_DBG ?= debian:bullseye + +GO_VERSION ?= 1.21 +BUILD_IMAGE ?= ghcr.io/appscode/golang-dev:$(GO_VERSION) + +OUTBIN = bin/$(OS)_$(ARCH)/$(BIN) +ifeq ($(OS),windows) + OUTBIN = bin/$(OS)_$(ARCH)/$(BIN).exe +endif + +# Directories that we need created to build/test. +BUILD_DIRS := bin/$(OS)_$(ARCH) \ + .go/bin/$(OS)_$(ARCH) \ + .go/cache \ + hack/config \ + $(HOME)/.credentials \ + $(HOME)/.kube \ + $(HOME)/.minikube + +DOCKERFILE_TEST = Dockerfile.test + +DOCKER_REPO_ROOT := /go/src/$(GO_PKG)/$(REPO) + +# If you want to build all binaries, see the 'all-build' rule. +# If you want to build all containers, see the 'all-container' rule. +# If you want to build AND push all containers, see the 'all-push' rule. +all: fmt build + +# For the following OS/ARCH expansions, we transform OS/ARCH into OS_ARCH +# because make pattern rules don't match with embedded '/' characters. + +build-%: + @$(MAKE) build \ + --no-print-directory \ + GOOS=$(firstword $(subst _, ,$*)) \ + GOARCH=$(lastword $(subst _, ,$*)) + +all-build: $(addprefix build-, $(subst /,_, $(BIN_PLATFORMS))) + +version: + @echo ::set-output name=version::$(VERSION) + @echo ::set-output name=version_strategy::$(version_strategy) + @echo ::set-output name=git_tag::$(git_tag) + @echo ::set-output name=git_branch::$(git_branch) + @echo ::set-output name=commit_hash::$(commit_hash) + @echo ::set-output name=commit_timestamp::$(commit_timestamp) + +# Generate a typed clientset +.PHONY: clientset +clientset: + @docker run --rm \ + -u $$(id -u):$$(id -g) \ + -v /tmp:/.cache \ + -v $$(pwd):$(DOCKER_REPO_ROOT) \ + -w $(DOCKER_REPO_ROOT) \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(CODE_GENERATOR_IMAGE) \ + /go/src/k8s.io/code-generator/generate-groups.sh \ + deepcopy-gen \ + $(GO_PKG)/$(REPO)/client \ + $(GO_PKG)/$(REPO)/apis \ + "$(API_GROUPS)" \ + --go-header-file "./hack/license/go.txt" + +.PHONY: gen +gen: clientset + +fmt: $(BUILD_DIRS) + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/bash -c " \ + REPO_PKG=$(GO_PKG) \ + ./hack/fmt.sh $(SRC_DIRS) \ + " + +build: $(OUTBIN) + +.PHONY: .go/$(OUTBIN) +$(OUTBIN): $(BUILD_DIRS) + @echo "making $(OUTBIN)" + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/bash -c " \ + ARCH=$(ARCH) \ + OS=$(OS) \ + VERSION=$(VERSION) \ + version_strategy=$(version_strategy) \ + git_branch=$(git_branch) \ + git_tag=$(git_tag) \ + commit_hash=$(commit_hash) \ + commit_timestamp=$(commit_timestamp) \ + ./hack/build.sh \ + " + @echo + +.PHONY: test +test: unit-tests + +unit-tests: $(BUILD_DIRS) + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + /bin/bash -c " \ + ARCH=$(ARCH) \ + OS=$(OS) \ + VERSION=$(VERSION) \ + ./hack/test.sh $(SRC_PKGS) \ + " + +ADDTL_LINTERS := goconst,gofmt,goimports,unparam + +.PHONY: lint +lint: $(BUILD_DIRS) + @echo "running linter" + @docker run \ + -i \ + --rm \ + -u $$(id -u):$$(id -g) \ + -v $$(pwd):/src \ + -w /src \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin \ + -v $$(pwd)/.go/bin/$(OS)_$(ARCH):/go/bin/$(OS)_$(ARCH) \ + -v $$(pwd)/.go/cache:/.cache \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + --env GO111MODULE=on \ + --env GOFLAGS="-mod=vendor" \ + $(BUILD_IMAGE) \ + golangci-lint run --enable $(ADDTL_LINTERS) --deadline=10m --skip-files="generated.*\.go$\" --skip-dirs-use-default --skip-dirs=client,vendor + +$(BUILD_DIRS): + @mkdir -p $@ + +.PHONY: dev +dev: gen fmt push + +.PHONY: verify +verify: verify-gen verify-modules + +.PHONY: verify-modules +verify-modules: + GO111MODULE=on go mod tidy + GO111MODULE=on go mod vendor + @if !(git diff --exit-code HEAD); then \ + echo "go module files are out of date"; exit 1; \ + fi + +.PHONY: verify-gen +verify-gen: gen fmt + @if !(git diff --exit-code HEAD); then \ + echo "generated files are out of date, run make gen"; exit 1; \ + fi + +.PHONY: add-license +add-license: + @echo "Adding license header" + @docker run --rm \ + -u $$(id -u):$$(id -g) \ + -v /tmp:/.cache \ + -v $$(pwd):$(DOCKER_REPO_ROOT) \ + -w $(DOCKER_REPO_ROOT) \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + ltag -t "./hack/license" --excludes "vendor contrib libbuild" -v + +.PHONY: check-license +check-license: + @echo "Checking files for license header" + @docker run --rm \ + -u $$(id -u):$$(id -g) \ + -v /tmp:/.cache \ + -v $$(pwd):$(DOCKER_REPO_ROOT) \ + -w $(DOCKER_REPO_ROOT) \ + --env HTTP_PROXY=$(HTTP_PROXY) \ + --env HTTPS_PROXY=$(HTTPS_PROXY) \ + $(BUILD_IMAGE) \ + ltag -t "./hack/license" --excludes "vendor contrib libbuild" --check -v + +.PHONY: ci +ci: verify check-license lint build unit-tests #cover + +.PHONY: clean +clean: + rm -rf .go bin diff --git a/vendor/go.bytebuilders.dev/license-verifier/README.md b/vendor/go.bytebuilders.dev/license-verifier/README.md new file mode 100644 index 0000000000..47c08bb70d --- /dev/null +++ b/vendor/go.bytebuilders.dev/license-verifier/README.md @@ -0,0 +1,4 @@ +[![PkgGoDev](https://pkg.go.dev/badge/go.bytebuilders.dev/license-verifier)](https://pkg.go.dev/go.bytebuilders.dev/license-verifier) + +# license-verifier +Library for verifying license diff --git a/vendor/go.bytebuilders.dev/license-verifier/lib.go b/vendor/go.bytebuilders.dev/license-verifier/lib.go new file mode 100644 index 0000000000..7ad58044c5 --- /dev/null +++ b/vendor/go.bytebuilders.dev/license-verifier/lib.go @@ -0,0 +1,214 @@ +/* +Copyright AppsCode Inc. + +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 verifier + +import ( + "crypto/x509" + "fmt" + "strings" + + "go.bytebuilders.dev/license-verifier/apis/licenses/v1alpha1" + "go.bytebuilders.dev/license-verifier/info" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" +) + +type Options struct { + ClusterUID string `json:"clusterUID"` + Features string `json:"features"` + CACert []byte `json:"caCert,omitempty"` + License []byte `json:"license"` +} + +type ParserOptions struct { + ClusterUID string + CACert *x509.Certificate + License []byte +} + +type VerifyOptions struct { + ParserOptions + Features string +} + +func ParseLicense(opts ParserOptions) (v1alpha1.License, error) { + cert, err := info.ParseCertificate(opts.License) + if err != nil { + return BadLicense(err) + } + + roots := x509.NewCertPool() + roots.AddCert(opts.CACert) + + crtopts := x509.VerifyOptions{ + DNSName: opts.ClusterUID, + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + }, + } + + // wildcard certificate + if strings.HasPrefix(cert.Subject.CommonName, "*.") { + if len(opts.CACert.Subject.Organization) > 0 { + crtopts.DNSName = "*." + opts.CACert.Subject.Organization[0] + } + } + + license := v1alpha1.License{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "License", + }, + Data: opts.License, + Issuer: "byte.builders", + Clusters: cert.DNSNames, + NotBefore: &metav1.Time{Time: cert.NotBefore}, + NotAfter: &metav1.Time{Time: cert.NotAfter}, + ID: cert.SerialNumber.String(), + Features: cert.Subject.Organization, + } + if len(cert.Subject.OrganizationalUnit) > 0 { + license.PlanName = cert.Subject.OrganizationalUnit[0] + } else { + // old certificate, so plan name auto detected from feature + // ref: https://github.com/appscode/offline-license-server/blob/v0.0.20/pkg/server/constants.go#L50-L59 + features := sets.NewString(cert.Subject.Organization...) + if features.Has("kubedb-enterprise") { + license.PlanName = "kubedb-enterprise" + } else if features.Has("kubedb-community") { + license.PlanName = "kubedb-community" + } else if features.Has("stash-enterprise") { + license.PlanName = "stash-enterprise" + } else if features.Has("stash-community") { + license.PlanName = "stash-community" + } + } + if len(cert.Subject.Country) > 0 { + license.ProductLine = cert.Subject.Country[0] + } + if len(cert.Subject.Province) > 0 { + license.TierName = cert.Subject.Province[0] + } + if license.ProductLine == "" || license.TierName == "" { + parts := strings.SplitN(license.PlanName, "-", 2) + if len(parts) > 0 { + license.ProductLine = parts[0] + } + if len(parts) > 1 { + license.TierName = parts[1] + } + } + license.FeatureFlags = map[string]string{} + for _, ff := range cert.Subject.Locality { + parts := strings.SplitN(ff, "=", 2) + if len(parts) == 2 { + license.FeatureFlags[parts[0]] = parts[1] + } + } + + var user *v1alpha1.User + for _, e := range cert.EmailAddresses { + parts := strings.FieldsFunc(e, func(r rune) bool { + return r == '<' || r == '>' + }) + if len(parts) == 0 { + continue + } + + if len(parts) == 1 { + email := strings.TrimSpace(parts[0]) + if user == nil { + user = &v1alpha1.User{ + Name: "", + Email: email, + } + } else if user.Email != email { + return BadLicense(fmt.Errorf("license issued to multiple emails %s", strings.Join(cert.EmailAddresses, ";"))) + } + } else { // == 2 + email := strings.TrimSpace(parts[1]) + if user == nil { + user = &v1alpha1.User{ + Name: strings.TrimSpace(parts[0]), + Email: email, + } + } else if user.Email != email { + return BadLicense(fmt.Errorf("license issued to multiple emails %s", strings.Join(cert.EmailAddresses, ";"))) + } + } + } + license.User = user + + // ref: https://github.com/appscode/gitea/blob/master/models/stripe_license.go#L117-L126 + if _, err := cert.Verify(crtopts); err != nil { + e2 := errors.Wrap(err, "failed to verify certificate") + license.Status = v1alpha1.LicenseInvalid + license.Reason = e2.Error() + return license, e2 + } + license.Status = v1alpha1.LicenseActive + return license, nil +} + +func CheckLicense(opts VerifyOptions) (v1alpha1.License, error) { + license, err := ParseLicense(opts.ParserOptions) + if err != nil { + return license, err + } + if !sets.NewString(license.Features...).HasAny(info.ParseFeatures(opts.Features)...) { + e2 := fmt.Errorf("license was not issued for %s", opts.Features) + license.Status = v1alpha1.LicenseInvalid + license.Reason = e2.Error() + return license, e2 + } + license.Status = v1alpha1.LicenseActive + return license, nil +} + +func VerifyLicense(opts Options) (v1alpha1.License, error) { + caCert, err := info.ParseCertificate(opts.CACert) + if err != nil { + return BadLicense(err) + } + + return CheckLicense(VerifyOptions{ + ParserOptions: ParserOptions{ + ClusterUID: opts.ClusterUID, + CACert: caCert, + License: opts.License, + }, + Features: opts.Features, + }) +} + +func BadLicense(err error) (v1alpha1.License, error) { + if err == nil { + // This should never happen + panic(err) + } + return v1alpha1.License{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "License", + }, + Status: v1alpha1.LicenseUnknown, + Reason: err.Error(), + }, err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3c64a5d120..53c1819505 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -765,6 +765,7 @@ github.com/yudai/golcs github.com/zeebo/xxh3 # go.bytebuilders.dev/license-verifier v0.14.0 ## explicit; go 1.21 +go.bytebuilders.dev/license-verifier go.bytebuilders.dev/license-verifier/apis/licenses go.bytebuilders.dev/license-verifier/apis/licenses/v1alpha1 go.bytebuilders.dev/license-verifier/info