Skip to content

Commit 3b9ac6d

Browse files
author
Simon Emms
committed
Extract the evaluator type and create Gitpod and Replicated licensors
1 parent 700746e commit 3b9ac6d

File tree

6 files changed

+242
-102
lines changed

6 files changed

+242
-102
lines changed

components/licensor/ee/cmd/validate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ var validateCmd = &cobra.Command{
3333
}
3434

3535
domain, _ := cmd.Flags().GetString("domain")
36-
e := licensor.NewEvaluator(lic, domain)
36+
e := licensor.NewGitpodEvaluator(lic, domain)
3737
if msg, valid := e.Validate(); !valid {
3838
return xerrors.Errorf(msg)
3939
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the Gitpod Enterprise Source Code License,
3+
// See License.enterprise.txt in the project root folder.
4+
5+
package licensor
6+
7+
import (
8+
"crypto"
9+
"crypto/rsa"
10+
"crypto/sha256"
11+
"encoding/base64"
12+
"encoding/json"
13+
"fmt"
14+
"time"
15+
)
16+
17+
// GitpodEvaluator determines what a license allows for
18+
type GitpodEvaluator struct {
19+
invalid string
20+
lic LicensePayload
21+
}
22+
23+
// Validate returns false if the license isn't valid and a message explaining why that is.
24+
func (e *GitpodEvaluator) Validate() (msg string, valid bool) {
25+
if e.invalid == "" {
26+
return "", true
27+
}
28+
29+
return e.invalid, false
30+
}
31+
32+
// Enabled determines if a feature is enabled by the license
33+
func (e *GitpodEvaluator) Enabled(feature Feature) bool {
34+
if e.invalid != "" {
35+
return false
36+
}
37+
38+
_, ok := e.lic.Level.allowance().Features[feature]
39+
return ok
40+
}
41+
42+
// HasEnoughSeats returns true if the license supports at least the give amount of seats
43+
func (e *GitpodEvaluator) HasEnoughSeats(seats int) bool {
44+
if e.invalid != "" {
45+
return false
46+
}
47+
48+
return e.lic.Seats == 0 || seats <= e.lic.Seats
49+
}
50+
51+
// Inspect returns the license information this evaluator holds.
52+
// This function is intended for transparency/debugging purposes only and must
53+
// never be used to determine feature eligibility under a license. All code making
54+
// those kinds of decisions must be part of the Evaluator.
55+
func (e *GitpodEvaluator) Inspect() LicensePayload {
56+
return e.lic
57+
}
58+
59+
// NewGitpodEvaluator produces a new license evaluator from a license key
60+
func NewGitpodEvaluator(key []byte, domain string) (res *GitpodEvaluator) {
61+
if len(key) == 0 {
62+
// fallback to the default license
63+
return &GitpodEvaluator{
64+
lic: defaultLicense,
65+
}
66+
}
67+
68+
deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key)))
69+
n, err := base64.StdEncoding.Decode(deckey, key)
70+
if err != nil {
71+
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)}
72+
}
73+
deckey = deckey[:n]
74+
75+
var lic licensePayload
76+
err = json.Unmarshal(deckey, &lic)
77+
if err != nil {
78+
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)}
79+
}
80+
81+
keyWoSig, err := json.Marshal(lic.LicensePayload)
82+
if err != nil {
83+
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)}
84+
}
85+
hashed := sha256.Sum256(keyWoSig)
86+
87+
for _, k := range publicKeys {
88+
err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature)
89+
if err == nil {
90+
break
91+
}
92+
}
93+
if err != nil {
94+
return &GitpodEvaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)}
95+
}
96+
97+
if !matchesDomain(lic.Domain, domain) {
98+
return &GitpodEvaluator{invalid: "wrong domain"}
99+
}
100+
101+
if lic.ValidUntil.Before(time.Now()) {
102+
return &GitpodEvaluator{invalid: "not valid anymore"}
103+
}
104+
105+
return &GitpodEvaluator{
106+
lic: lic.LicensePayload,
107+
}
108+
}

components/licensor/ee/pkg/licensor/licensor.go

Lines changed: 5 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -115,57 +115,6 @@ var defaultLicense = LicensePayload{
115115
// Domain, ValidUntil are free for all
116116
}
117117

118-
// NewEvaluator produces a new license evaluator from a license key
119-
func NewEvaluator(key []byte, domain string) (res *Evaluator) {
120-
if len(key) == 0 {
121-
// fallback to the default license
122-
return &Evaluator{
123-
lic: defaultLicense,
124-
}
125-
}
126-
127-
deckey := make([]byte, base64.StdEncoding.DecodedLen(len(key)))
128-
n, err := base64.StdEncoding.Decode(deckey, key)
129-
if err != nil {
130-
return &Evaluator{invalid: fmt.Sprintf("cannot decode key: %q", err)}
131-
}
132-
deckey = deckey[:n]
133-
134-
var lic licensePayload
135-
err = json.Unmarshal(deckey, &lic)
136-
if err != nil {
137-
return &Evaluator{invalid: fmt.Sprintf("cannot unmarshal key: %q", err)}
138-
}
139-
140-
keyWoSig, err := json.Marshal(lic.LicensePayload)
141-
if err != nil {
142-
return &Evaluator{invalid: fmt.Sprintf("cannot remarshal key: %q", err)}
143-
}
144-
hashed := sha256.Sum256(keyWoSig)
145-
146-
for _, k := range publicKeys {
147-
err = rsa.VerifyPKCS1v15(k, crypto.SHA256, hashed[:], lic.Signature)
148-
if err == nil {
149-
break
150-
}
151-
}
152-
if err != nil {
153-
return &Evaluator{invalid: fmt.Sprintf("cannot verify key: %q", err)}
154-
}
155-
156-
if !matchesDomain(lic.Domain, domain) {
157-
return &Evaluator{invalid: "wrong domain"}
158-
}
159-
160-
if lic.ValidUntil.Before(time.Now()) {
161-
return &Evaluator{invalid: "not valid anymore"}
162-
}
163-
164-
return &Evaluator{
165-
lic: lic.LicensePayload,
166-
}
167-
}
168-
169118
func matchesDomain(pattern, domain string) bool {
170119
if pattern == "" {
171120
return true
@@ -184,46 +133,11 @@ func matchesDomain(pattern, domain string) bool {
184133
return false
185134
}
186135

187-
// Evaluator determines what a license allows for
188-
type Evaluator struct {
189-
invalid string
190-
lic LicensePayload
191-
}
192-
193-
// Validate returns false if the license isn't valid and a message explaining why that is.
194-
func (e *Evaluator) Validate() (msg string, valid bool) {
195-
if e.invalid == "" {
196-
return "", true
197-
}
198-
199-
return e.invalid, false
200-
}
201-
202-
// Enabled determines if a feature is enabled by the license
203-
func (e *Evaluator) Enabled(feature Feature) bool {
204-
if e.invalid != "" {
205-
return false
206-
}
207-
208-
_, ok := e.lic.Level.allowance().Features[feature]
209-
return ok
210-
}
211-
212-
// HasEnoughSeats returns true if the license supports at least the give amount of seats
213-
func (e *Evaluator) HasEnoughSeats(seats int) bool {
214-
if e.invalid != "" {
215-
return false
216-
}
217-
218-
return e.lic.Seats == 0 || seats <= e.lic.Seats
219-
}
220-
221-
// Inspect returns the license information this evaluator holds.
222-
// This function is intended for transparency/debugging purposes only and must
223-
// never be used to determine feature eligibility under a license. All code making
224-
// those kinds of decisions must be part of the Evaluator.
225-
func (e *Evaluator) Inspect() LicensePayload {
226-
return e.lic
136+
type Evaluator interface {
137+
Enabled(feature Feature) bool
138+
HasEnoughSeats(seats int) bool
139+
Inspect() LicensePayload
140+
Validate() (msg string, valid bool)
227141
}
228142

229143
// Sign signs a license so that it can be used with the evaluator

components/licensor/ee/pkg/licensor/licensor_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ const (
2020
type licenseTest struct {
2121
Name string
2222
License *LicensePayload
23-
Validate func(t *testing.T, eval *Evaluator)
23+
Validate func(t *testing.T, eval *GitpodEvaluator)
2424
}
2525

2626
func (test *licenseTest) Run(t *testing.T) {
2727
t.Run(test.Name, func(t *testing.T) {
28-
var eval *Evaluator
28+
var eval *GitpodEvaluator
2929
if test.License == nil {
30-
eval = NewEvaluator(nil, "")
30+
eval = NewGitpodEvaluator(nil, "")
3131
} else {
3232
priv, err := rsa.GenerateKey(rand.Reader, 2048)
3333
if err != nil {
@@ -39,7 +39,7 @@ func (test *licenseTest) Run(t *testing.T) {
3939
t.Fatalf("cannot sign license: %q", err)
4040
}
4141

42-
eval = NewEvaluator(lic, domain)
42+
eval = NewGitpodEvaluator(lic, domain)
4343
}
4444

4545
test.Validate(t, eval)
@@ -81,7 +81,7 @@ func TestSeats(t *testing.T) {
8181
Seats: test.Licensed,
8282
ValidUntil: validUntil,
8383
},
84-
Validate: func(t *testing.T, eval *Evaluator) {
84+
Validate: func(t *testing.T, eval *GitpodEvaluator) {
8585
withinLimits := eval.HasEnoughSeats(test.Probe)
8686
if withinLimits != test.WithinLimits {
8787
t.Errorf("HasEnoughSeats did not behave as expected: lic=%d probe=%d expected=%v actual=%v", test.Licensed, test.Probe, test.WithinLimits, withinLimits)
@@ -127,7 +127,7 @@ func TestFeatures(t *testing.T) {
127127
lt := licenseTest{
128128
Name: test.Name,
129129
License: lic,
130-
Validate: func(t *testing.T, eval *Evaluator) {
130+
Validate: func(t *testing.T, eval *GitpodEvaluator) {
131131
unavailableFeatures := featureSet{}
132132
for f := range allowanceMap[LevelEnterprise].Features {
133133
unavailableFeatures[f] = struct{}{}
@@ -258,7 +258,7 @@ func TestEvalutorKeys(t *testing.T) {
258258
}
259259

260260
var errmsg string
261-
e := NewEvaluator(lic, dom)
261+
e := NewGitpodEvaluator(lic, dom)
262262
if msg, valid := e.Validate(); !valid {
263263
errmsg = msg
264264
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the Gitpod Enterprise Source Code License,
3+
// See License.enterprise.txt in the project root folder.
4+
5+
package licensor
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"net/http"
11+
"time"
12+
)
13+
14+
type replicatedFields struct {
15+
Field string `json:"field"`
16+
Title string `json:"title"`
17+
Type string `json:"type"`
18+
Value interface{} `json:"value"` // This is of type "fieldType"
19+
}
20+
21+
// ReplicatedLicensePayload exists to convert the JSON structure to a LicensePayload
22+
type replicatedLicensePayload struct {
23+
LicenseID string `json:"license_id"`
24+
InstallationID string `json:"installation_id"`
25+
Assignee string `json:"assignee"`
26+
ReleaseChannel string `json:"release_channel"`
27+
LicenseType string `json:"license_type"`
28+
ExpirationTime time.Time `json:"expiration_time"`
29+
Fields []replicatedFields `json:"fields"`
30+
}
31+
32+
type ReplicatedEvaluator struct {
33+
invalid string
34+
lic LicensePayload
35+
}
36+
37+
func (e *ReplicatedEvaluator) Enabled(feature Feature) bool {
38+
if e.invalid != "" {
39+
return false
40+
}
41+
42+
_, ok := e.lic.Level.allowance().Features[feature]
43+
return ok
44+
}
45+
46+
func (e *ReplicatedEvaluator) HasEnoughSeats(seats int) bool {
47+
if e.invalid != "" {
48+
return false
49+
}
50+
51+
return e.lic.Seats == 0 || seats <= e.lic.Seats
52+
}
53+
54+
func (e *ReplicatedEvaluator) Inspect() LicensePayload {
55+
return e.lic
56+
}
57+
58+
func (e *ReplicatedEvaluator) Validate() (msg string, valid bool) {
59+
if e.invalid == "" {
60+
return "", true
61+
}
62+
63+
return e.invalid, false
64+
}
65+
66+
// NewReplicatedEvaluator get the license data from the kots admin panel
67+
func NewReplicatedEvaluator(domain string) (res *ReplicatedEvaluator) {
68+
client := http.Client{Timeout: 5 * time.Second}
69+
resp, err := client.Get("http://kotsadm:3000/license/v1/license")
70+
if err != nil {
71+
return &ReplicatedEvaluator{invalid: fmt.Sprintf("cannot query kots admin, %q", err)}
72+
}
73+
defer resp.Body.Close()
74+
75+
var replicatedPayload replicatedLicensePayload
76+
err = json.NewDecoder(resp.Body).Decode(&replicatedPayload)
77+
if err != nil {
78+
return &ReplicatedEvaluator{invalid: fmt.Sprintf("cannot decode json data, %q", err)}
79+
}
80+
81+
lic := LicensePayload{
82+
ID: replicatedPayload.LicenseID,
83+
ValidUntil: replicatedPayload.ExpirationTime,
84+
}
85+
86+
// Search for the fields
87+
for _, i := range replicatedPayload.Fields {
88+
switch i.Field {
89+
case "domain":
90+
lic.Domain = i.Value.(string)
91+
92+
case "level":
93+
lic.Level = LicenseLevel(i.Value.(float64))
94+
95+
case "seats":
96+
lic.Seats = int(i.Value.(float64))
97+
}
98+
}
99+
100+
if !matchesDomain(lic.Domain, domain) {
101+
return &ReplicatedEvaluator{invalid: "wrong domain"}
102+
}
103+
104+
if lic.ValidUntil.Before(time.Now()) {
105+
return &ReplicatedEvaluator{invalid: "not valid anymore"}
106+
}
107+
108+
return &ReplicatedEvaluator{
109+
lic: lic,
110+
}
111+
}

0 commit comments

Comments
 (0)