diff --git a/changelog/v1.18.0-beta14/glooctl-license-check.yaml b/changelog/v1.18.0-beta14/glooctl-license-check.yaml new file mode 100644 index 00000000000..96c63eff5fa --- /dev/null +++ b/changelog/v1.18.0-beta14/glooctl-license-check.yaml @@ -0,0 +1,6 @@ +changelog: + - type: NEW_FEATURE + issueLink: https://github.com/solo-io/gloo/issues/3520 + resolvesIssue: false + description: >- + Check the validity of Gloo Gateway License using `glooctl license validate --license-key `. diff --git a/docs/content/reference/cli/glooctl.md b/docs/content/reference/cli/glooctl.md index f662918a321..d12f88d87bf 100644 --- a/docs/content/reference/cli/glooctl.md +++ b/docs/content/reference/cli/glooctl.md @@ -45,6 +45,7 @@ glooctl is the unified CLI for Gloo. * [glooctl init-plugin-manager](../glooctl_init-plugin-manager) - Install the Gloo Gateway Enterprise CLI plugin manager * [glooctl install](../glooctl_install) - install gloo on different platforms * [glooctl istio](../glooctl_istio) - Commands for interacting with Istio in Gloo +* [glooctl license](../glooctl_license) - subcommands for interacting with the license * [glooctl plugin](../glooctl_plugin) - Commands for interacting with glooctl plugins * [glooctl proxy](../glooctl_proxy) - interact with proxy instances managed by Gloo * [glooctl remove](../glooctl_remove) - remove configuration items from a top-level Gloo resource diff --git a/docs/content/reference/cli/glooctl_license.md b/docs/content/reference/cli/glooctl_license.md new file mode 100644 index 00000000000..b901710a7cc --- /dev/null +++ b/docs/content/reference/cli/glooctl_license.md @@ -0,0 +1,35 @@ +--- +title: "glooctl license" +weight: 5 +--- +## glooctl license + +subcommands for interacting with the license + +### Options + +``` + -h, --help help for license +``` + +### Options inherited from parent commands + +``` + -c, --config string set the path to the glooctl config file (default "/.gloo/glooctl-config.yaml") + --consul-address string address of the Consul server. Use with --use-consul (default "127.0.0.1:8500") + --consul-allow-stale-reads Allows reading using Consul's stale consistency mode. + --consul-datacenter string Datacenter to use. If not provided, the default agent datacenter is used. Use with --use-consul + --consul-root-key string key prefix for for Consul key-value storage. (default "gloo") + --consul-scheme string URI scheme for the Consul server. Use with --use-consul (default "http") + --consul-token string Token is used to provide a per-request ACL token which overrides the agent's default token. Use with --use-consul + -i, --interactive use interactive mode + --kube-context string kube context to use when interacting with kubernetes + --kubeconfig string kubeconfig to use, if not standard one + --use-consul use Consul Key-Value storage as the backend for reading and writing config (VirtualServices, Upstreams, and Proxies) +``` + +### SEE ALSO + +* [glooctl](../glooctl) - CLI for Gloo +* [glooctl license validate](../glooctl_license_validate) - Check Gloo Gateway License Validity + diff --git a/docs/content/reference/cli/glooctl_license_validate.md b/docs/content/reference/cli/glooctl_license_validate.md new file mode 100644 index 00000000000..a17d6c95595 --- /dev/null +++ b/docs/content/reference/cli/glooctl_license_validate.md @@ -0,0 +1,45 @@ +--- +title: "glooctl license validate" +weight: 5 +--- +## glooctl license validate + +Check Gloo Gateway License Validity + +### Synopsis + +Checking Gloo Gateway license Validity. + +Usage: `glooctl license validate [--license-key license-key]` + +``` +glooctl license validate [flags] +``` + +### Options + +``` + -h, --help help for validate + --license-key string license key to validate +``` + +### Options inherited from parent commands + +``` + -c, --config string set the path to the glooctl config file (default "/.gloo/glooctl-config.yaml") + --consul-address string address of the Consul server. Use with --use-consul (default "127.0.0.1:8500") + --consul-allow-stale-reads Allows reading using Consul's stale consistency mode. + --consul-datacenter string Datacenter to use. If not provided, the default agent datacenter is used. Use with --use-consul + --consul-root-key string key prefix for for Consul key-value storage. (default "gloo") + --consul-scheme string URI scheme for the Consul server. Use with --use-consul (default "http") + --consul-token string Token is used to provide a per-request ACL token which overrides the agent's default token. Use with --use-consul + -i, --interactive use interactive mode + --kube-context string kube context to use when interacting with kubernetes + --kubeconfig string kubeconfig to use, if not standard one + --use-consul use Consul Key-Value storage as the backend for reading and writing config (VirtualServices, Upstreams, and Proxies) +``` + +### SEE ALSO + +* [glooctl license](../glooctl_license) - subcommands for interacting with the license + diff --git a/docs/content/static/content/osa_provided.md b/docs/content/static/content/osa_provided.md index 883c8b93261..8c5b51b5f32 100644 --- a/docs/content/static/content/osa_provided.md +++ b/docs/content/static/content/osa_provided.md @@ -21,6 +21,7 @@ Name|Version|License [go-swagger/go-swagger](https://github.com/go-swagger/go-swagger)|v0.21.0|Apache License 2.0 [gogo/googleapis](https://github.com/gogo/googleapis)|v1.4.0|Apache License 2.0 [gogo/protobuf](https://github.com/gogo/protobuf)|v1.3.2|BSD 3-clause "New" or "Revised" License +[jwt/v4](https://github.com/golang-jwt/jwt)|v4.5.0|MIT License [golang/protobuf](https://github.com/golang/protobuf)|v1.5.3|BSD 3-clause "New" or "Revised" License [google/go-cmp](https://github.com/google/go-cmp)|v0.6.0|BSD 3-clause "New" or "Revised" License [google/go-github](https://github.com/google/go-github)|v17.0.0+incompatible|BSD 3-clause "New" or "Revised" License diff --git a/go.mod b/go.mod index c8c89d3635d..8796a509c09 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.1-0.20240214155107-6cf1ede4da61 github.com/avast/retry-go/v4 v4.3.3 github.com/go-logr/zapr v1.3.0 + github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.3.1 diff --git a/go.sum b/go.sum index 67db4704e7b..1e73d8a15d5 100644 --- a/go.sum +++ b/go.sum @@ -1236,6 +1236,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= diff --git a/projects/gloo/cli/pkg/cmd/license/license_suite_test.go b/projects/gloo/cli/pkg/cmd/license/license_suite_test.go new file mode 100644 index 00000000000..917efb7f0e0 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/license/license_suite_test.go @@ -0,0 +1,13 @@ +package license + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestLicense(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "License Suite") +} diff --git a/projects/gloo/cli/pkg/cmd/license/root.go b/projects/gloo/cli/pkg/cmd/license/root.go new file mode 100644 index 00000000000..ce3ce11c7f8 --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/license/root.go @@ -0,0 +1,16 @@ +package license + +import ( + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/options" + "github.com/spf13/cobra" +) + +func RootCmd(opts *options.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "license", + Aliases: []string{"l"}, + Short: "subcommands for interacting with the license", + } + cmd.AddCommand(License(opts)) + return cmd +} diff --git a/projects/gloo/cli/pkg/cmd/license/validate.go b/projects/gloo/cli/pkg/cmd/license/validate.go new file mode 100644 index 00000000000..def85a57a1d --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/license/validate.go @@ -0,0 +1,105 @@ +package license + +import ( + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/options" + "github.com/solo-io/gloo/projects/gloo/cli/pkg/flagutils" + "github.com/spf13/cobra" +) + +type AddOn struct { + Addon int `json:"Addon"` + ExpiresAt int64 `json:"ExpiresAt"` + LicenseType string `json:"LicenseType"` +} + +type LicenseClaims struct { + AddOns []AddOn `json:"addOns"` + ExpirationDate int64 `json:"exp"` + CreationDate int64 `json:"iat"` + LicenseType string `json:"lt"` + Product string `json:"product"` + jwt.RegisteredClaims +} + +type LicenseLegacyClaims struct { + AddOns string `json:"addOns"` + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` + Key string `json:"k"` + LicType string `json:"lt"` + Product string `json:"product"` + jwt.RegisteredClaims +} + +func License(opts *options.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "validate", + Aliases: []string{"v", "validate"}, + Short: "Check Gloo Gateway License Validity", + Long: "Checking Gloo Gateway license Validity.\n\n" + + "" + + "Usage: `glooctl license validate [--license-key license-key]`", + + RunE: func(cmd *cobra.Command, args []string) error { + licenseKey := opts.ValidateLicense.LicenseKey + if strings.Count(licenseKey, ".") == 1 { + return validateLegacyLicense(opts.ValidateLicense.LicenseKey) + } + return validateLicense(opts.ValidateLicense.LicenseKey) + + }} + flags := cmd.Flags() + flagutils.AddLicenseValidationFlag(flags, &opts.ValidateLicense.LicenseKey) + cmd.MarkFlagRequired(flagutils.LicenseFlag) + return cmd +} + +func validateLicense(licenseKey string) error { + var licenseClaims LicenseClaims + + _, _, err := new(jwt.Parser).ParseUnverified(licenseKey, &licenseClaims) + if err != nil { + return fmt.Errorf("can't parse license key") + } + fmt.Printf(formatLicenseDetail(licenseClaims.CreationDate, licenseClaims.ExpirationDate, licenseClaims.Product, licenseClaims.LicenseType == "trial")) + return nil +} + +func validateLegacyLicense(licenseKey string) error { + var licenseLegacyClaim LicenseLegacyClaims + var standardizedLicenseKey = base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) + "." + licenseKey + _, _, err := new(jwt.Parser).ParseUnverified(standardizedLicenseKey, &licenseLegacyClaim) + if err != nil { + return fmt.Errorf("can't parse license key") + } + fmt.Printf(formatLicenseDetail(licenseLegacyClaim.Iat, licenseLegacyClaim.Exp, licenseLegacyClaim.Product, licenseLegacyClaim.LicType == "trial")) + return nil +} + +func formatLicenseDetail(creationTime int64, expirationTime int64, product string, isTrial bool) string { + var productName = "unknown" + switch product { + case "gloo": + productName = "Gloo Gateway" + } + var res = "" + if isTrial { + res += fmt.Sprintln("This a trial license for", productName) + } else { + res += fmt.Sprintln("This an enterprise license for", productName) + } + res += fmt.Sprintln("This license was created on:", time.Unix(creationTime, 0)) + if expirationTime < time.Now().Unix() { + res += fmt.Sprintln("This license is expired since:", time.Unix(expirationTime, 0)) + } else { + res += fmt.Sprintln("This license is valid until:", time.Unix(expirationTime, 0)) + } + + return res +} diff --git a/projects/gloo/cli/pkg/cmd/license/validate_test.go b/projects/gloo/cli/pkg/cmd/license/validate_test.go new file mode 100644 index 00000000000..dbd8dc07ecc --- /dev/null +++ b/projects/gloo/cli/pkg/cmd/license/validate_test.go @@ -0,0 +1,59 @@ +package license + +import ( + "time" + + "github.com/golang-jwt/jwt/v4" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("License Validate", func() { + + licenseClaims := LicenseClaims{ + AddOns: []AddOn{{Addon: 1, ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), LicenseType: "trial"}}, + ExpirationDate: time.Now().Add(24 * time.Hour).Unix(), + CreationDate: time.Now().Add(-24 * time.Hour).Unix(), + LicenseType: "trial", + Product: "gloo", + } + + expiredLicenseClaims := LicenseClaims{ + AddOns: []AddOn{{Addon: 1, ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), LicenseType: "trial"}}, + ExpirationDate: time.Now().Add(-24 * time.Hour).Unix(), + CreationDate: time.Now().Add(-48 * time.Hour).Unix(), + LicenseType: "ent", + Product: "gloo", + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, licenseClaims) + tokenString, err := token.SignedString([]byte("secret")) + + It("should verify a valid license", func() { + err = validateLicense(tokenString) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should fail to verify an invalid license", func() { + invalidLicenseKey := "invalid" + + err := validateLicense(invalidLicenseKey) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("can't parse license key")) + }) + + It("should print correct values for valid license", func() { + + res := formatLicenseDetail(licenseClaims.CreationDate, licenseClaims.ExpirationDate, licenseClaims.Product, licenseClaims.LicenseType == "trial") + Expect(res).To(ContainSubstring("This a trial license")) + Expect(res).To(ContainSubstring("This license is valid until")) + }) + + It("should print correct values for expired license", func() { + + res := formatLicenseDetail(expiredLicenseClaims.CreationDate, expiredLicenseClaims.ExpirationDate, expiredLicenseClaims.Product, expiredLicenseClaims.LicenseType == "trial") + Expect(res).To(ContainSubstring("This an enterprise license for Gloo Gateway")) + Expect(res).To(ContainSubstring("This license is expired since")) + }) + +}) diff --git a/projects/gloo/cli/pkg/cmd/options/options.go b/projects/gloo/cli/pkg/cmd/options/options.go index 225f5f2823e..e5106888368 100644 --- a/projects/gloo/cli/pkg/cmd/options/options.go +++ b/projects/gloo/cli/pkg/cmd/options/options.go @@ -18,23 +18,24 @@ import ( ) type Options struct { - Metadata core.Metadata - Top Top - Install Install - Uninstall Uninstall - Proxy Proxy - Upgrade Upgrade - Create Create - Delete Delete - Edit Edit - Route Route - Get Get - Add Add - Istio Istio - Remove Remove - Cluster Cluster - Check Check - CheckCRD CheckCRD + Metadata core.Metadata + Top Top + Install Install + Uninstall Uninstall + Proxy Proxy + Upgrade Upgrade + Create Create + Delete Delete + Edit Edit + Route Route + Get Get + Add Add + Istio Istio + Remove Remove + Cluster Cluster + Check Check + CheckCRD CheckCRD + ValidateLicense ValidateLicense } type Top struct { contextoptions.ContextAccessible @@ -491,3 +492,7 @@ type CheckCRD struct { LocalChart string ShowYaml bool } + +type ValidateLicense struct { + LicenseKey string +} diff --git a/projects/gloo/cli/pkg/cmd/root.go b/projects/gloo/cli/pkg/cmd/root.go index a333eb50e67..dccafd9421c 100644 --- a/projects/gloo/cli/pkg/cmd/root.go +++ b/projects/gloo/cli/pkg/cmd/root.go @@ -20,6 +20,7 @@ import ( "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/initpluginmanager" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/install" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/istio" + "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/license" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/options" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/plugin" "github.com/solo-io/gloo/projects/gloo/cli/pkg/cmd/remove" @@ -128,6 +129,7 @@ func CommandWithContext(ctx context.Context) *cobra.Command { federation.RootCmd(opts), plugin.RootCmd(opts), istio.RootCmd(opts), + license.RootCmd(opts), initpluginmanager.Command(context.Background()), // TODO: re-enable this when it's working again // kubegateway.InstallCmd(opts), diff --git a/projects/gloo/cli/pkg/flagutils/install.go b/projects/gloo/cli/pkg/flagutils/install.go index 966a8bc2e74..64732d5d8fb 100644 --- a/projects/gloo/cli/pkg/flagutils/install.go +++ b/projects/gloo/cli/pkg/flagutils/install.go @@ -19,7 +19,7 @@ func AddGlooInstallFlags(set *pflag.FlagSet, install *options.Install) { func AddEnterpriseInstallFlags(set *pflag.FlagSet, install *options.Install) { set.BoolVarP(&install.DryRun, "dry-run", "d", false, "Dump the raw installation yaml instead of applying it to kubernetes") - set.StringVar(&install.LicenseKey, "license-key", "", "License key to activate GlooE features") + set.StringVar(&install.LicenseKey, LicenseFlag, "", "License key to activate GlooE features") set.BoolVar(&install.WithGlooFed, "with-gloo-fed", true, "Install Gloo-Fed alongside Gloo Enterprise") // Gloo-fed set.StringSliceVar(&install.Federation.HelmChartValueFileNames, "gloo-fed-values", []string{}, "List of files with value overrides for the Gloo Fed Helm chart, (e.g. --values file1,file2 or --values file1 --values file2)") diff --git a/projects/gloo/cli/pkg/flagutils/license.go b/projects/gloo/cli/pkg/flagutils/license.go new file mode 100644 index 00000000000..3a36ef46ae2 --- /dev/null +++ b/projects/gloo/cli/pkg/flagutils/license.go @@ -0,0 +1,11 @@ +package flagutils + +import "github.com/spf13/pflag" + +const ( + LicenseFlag = "license-key" +) + +func AddLicenseValidationFlag(set *pflag.FlagSet, strptr *string) { + set.StringVarP(strptr, LicenseFlag, "", "", "license key to validate") +} diff --git a/test/kube2e/helper/install.go b/test/kube2e/helper/install.go index 7e41682e201..23bef2d03a3 100644 --- a/test/kube2e/helper/install.go +++ b/test/kube2e/helper/install.go @@ -73,7 +73,7 @@ type TestConfig struct { InstallNamespace string // Name of the glooctl executable GlooctlExecName string - // If provided, the licence key to install the enterprise version of Gloo + // If provided, the license key to install the enterprise version of Gloo LicenseKey string // Determines whether the test server pod gets deployed DeployTestServer bool diff --git a/test/kubernetes/testutils/helper/install.go b/test/kubernetes/testutils/helper/install.go index 1f257a933a9..0ac8a48f24b 100644 --- a/test/kubernetes/testutils/helper/install.go +++ b/test/kubernetes/testutils/helper/install.go @@ -68,7 +68,7 @@ type TestConfig struct { InstallNamespace string // Name of the glooctl executable GlooctlExecName string - // If provided, the licence key to install the enterprise version of Gloo + // If provided, the license key to install the enterprise version of Gloo LicenseKey string // Install a released version of gloo. This is the value of the github tag that may have a leading 'v' ReleasedVersion string