Skip to content

Commit

Permalink
Extract the evaluator type and create Gitpod and Replicated licensors
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Emms committed Feb 21, 2022
1 parent 535d191 commit 558d979
Show file tree
Hide file tree
Showing 6 changed files with 436 additions and 137 deletions.
32 changes: 23 additions & 9 deletions components/licensor/ee/cmd/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"io"
"os"

// "io"
// "os"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

Expand All @@ -22,18 +25,28 @@ var validateCmd = &cobra.Command{
Short: "Validates a license - reads from stdin if no argument is provided",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
var lic []byte
if len(args) == 0 {
lic, err = io.ReadAll(os.Stdin)
if err != nil {
return err
domain, _ := cmd.Flags().GetString("domain")
licensorType, _ := cmd.Flags().GetString("licensor")

var e licensor.Evaluator
switch licensorType {
case string(licensor.LicenseTypeReplicated):
e = licensor.NewReplicatedEvaluator(domain)
break
default:
var lic []byte
if len(args) == 0 {
lic, err = io.ReadAll(os.Stdin)
if err != nil {
return err
}
} else {
lic = []byte(args[0])
}
} else {
lic = []byte(args[0])

e = licensor.NewGitpodEvaluator(lic, domain)
}

domain, _ := cmd.Flags().GetString("domain")
e := licensor.NewEvaluator(lic, domain)
if msg, valid := e.Validate(); !valid {
return xerrors.Errorf(msg)
}
Expand All @@ -47,4 +60,5 @@ var validateCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(validateCmd)
validateCmd.Flags().String("domain", "", "domain to evaluate the license against")
validateCmd.Flags().String("licensor", "gitpod", "licensor to use")
}
108 changes: 108 additions & 0 deletions components/licensor/ee/pkg/licensor/gitpod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the Gitpod Enterprise Source Code License,
// See License.enterprise.txt in the project root folder.

package licensor

import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"time"
)

// GitpodEvaluator determines what a license allows for
type GitpodEvaluator struct {
invalid string
lic LicensePayload
}

// Validate returns false if the license isn't valid and a message explaining why that is.
func (e *GitpodEvaluator) Validate() (msg string, valid bool) {
if e.invalid == "" {
return "", true
}

return e.invalid, false
}

// Enabled determines if a feature is enabled by the license
func (e *GitpodEvaluator) Enabled(feature Feature) bool {
if e.invalid != "" {
return false
}

_, ok := e.lic.Level.allowance().Features[feature]
return ok
}

// HasEnoughSeats returns true if the license supports at least the give amount of seats
func (e *GitpodEvaluator) HasEnoughSeats(seats int) bool {
if e.invalid != "" {
return false
}

return e.lic.Seats == 0 || seats <= e.lic.Seats
}

// Inspect returns the license information this evaluator holds.
// This function is intended for transparency/debugging purposes only and must
// never be used to determine feature eligibility under a license. All code making
// those kinds of decisions must be part of the Evaluator.
func (e *GitpodEvaluator) Inspect() LicensePayload {
return e.lic
}

// NewGitpodEvaluator produces a new license evaluator from a license key
func NewGitpodEvaluator(key []byte, domain string) (res *GitpodEvaluator) {
if len(key) == 0 {
// fallback to the default license
return &GitpodEvaluator{
lic: defaultLicense,
}
}

deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key)))
n, err := base64.StdEncoding.Decode(deckey, key)
if err != nil {
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)}
}
deckey = deckey[:n]

var lic licensePayload
err = json.Unmarshal(deckey, &lic)
if err != nil {
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)}
}

keyWoSig, err := json.Marshal(lic.LicensePayload)
if err != nil {
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)}
}
hashed := sha256.Sum256(keyWoSig)

for _, k := range publicKeys {
err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature)
if err == nil {
break
}
}
if err != nil {
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)}
}

if !matchesDomain(lic.Domain, domain) {
return &GitpodEvaluator{invalid: "wrong domain"}
}

if lic.ValidUntil.Before(time.Now()) {
return &GitpodEvaluator{invalid: "not valid anymore"}
}

return &GitpodEvaluator{
lic: lic.LicensePayload,
}
}
103 changes: 12 additions & 91 deletions components/licensor/ee/pkg/licensor/licensor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import (
"time"
)

type LicenseType string

const (
LicenseTypeGitpod LicenseType = "gitpod"
LicenseTypeReplicated LicenseType = "replicated"
)

// LicensePayload is the actual license content
type LicensePayload struct {
ID string `json:"id"`
Expand Down Expand Up @@ -115,57 +122,6 @@ var defaultLicense = LicensePayload{
// Domain, ValidUntil are free for all
}

// NewEvaluator produces a new license evaluator from a license key
func NewEvaluator(key []byte, domain string) (res *Evaluator) {
if len(key) == 0 {
// fallback to the default license
return &Evaluator{
lic: defaultLicense,
}
}

deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key)))
n, err := base64.StdEncoding.Decode(deckey, key)
if err != nil {
return &Evaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)}
}
deckey = deckey[:n]

var lic licensePayload
err = json.Unmarshal(deckey, &lic)
if err != nil {
return &Evaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)}
}

keyWoSig, err := json.Marshal(lic.LicensePayload)
if err != nil {
return &Evaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)}
}
hashed := sha256.Sum256(keyWoSig)

for _, k := range publicKeys {
err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature)
if err == nil {
break
}
}
if err != nil {
return &Evaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)}
}

if !matchesDomain(lic.Domain, domain) {
return &Evaluator{invalid: "wrong domain"}
}

if lic.ValidUntil.Before(time.Now()) {
return &Evaluator{invalid: "not valid anymore"}
}

return &Evaluator{
lic: lic.LicensePayload,
}
}

func matchesDomain(pattern, domain string) bool {
if pattern == "" {
return true
Expand All @@ -184,46 +140,11 @@ func matchesDomain(pattern, domain string) bool {
return false
}

// Evaluator determines what a license allows for
type Evaluator struct {
invalid string
lic LicensePayload
}

// Validate returns false if the license isn't valid and a message explaining why that is.
func (e *Evaluator) Validate() (msg string, valid bool) {
if e.invalid == "" {
return "", true
}

return e.invalid, false
}

// Enabled determines if a feature is enabled by the license
func (e *Evaluator) Enabled(feature Feature) bool {
if e.invalid != "" {
return false
}

_, ok := e.lic.Level.allowance().Features[feature]
return ok
}

// HasEnoughSeats returns true if the license supports at least the give amount of seats
func (e *Evaluator) HasEnoughSeats(seats int) bool {
if e.invalid != "" {
return false
}

return e.lic.Seats == 0 || seats <= e.lic.Seats
}

// Inspect returns the license information this evaluator holds.
// This function is intended for transparency/debugging purposes only and must
// never be used to determine feature eligibility under a license. All code making
// those kinds of decisions must be part of the Evaluator.
func (e *Evaluator) Inspect() LicensePayload {
return e.lic
type Evaluator interface {
Enabled(feature Feature) bool
HasEnoughSeats(seats int) bool
Inspect() LicensePayload
Validate() (msg string, valid bool)
}

// Sign signs a license so that it can be used with the evaluator
Expand Down
Loading

0 comments on commit 558d979

Please sign in to comment.