diff --git a/components/licensor/ee/pkg/licensor/gitpod.go b/components/licensor/ee/pkg/licensor/gitpod.go index fe3bc76fc72818..f9ef40a68f8b55 100644 --- a/components/licensor/ee/pkg/licensor/gitpod.go +++ b/components/licensor/ee/pkg/licensor/gitpod.go @@ -62,5 +62,6 @@ func NewGitpodEvaluator(key []byte, domain string) (res *Evaluator) { return &Evaluator{ lic: lic.LicensePayload, + noFallback: true, // } } diff --git a/components/licensor/ee/pkg/licensor/licensor.go b/components/licensor/ee/pkg/licensor/licensor.go index d0a447fd70e9b4..0999a2fbccdef1 100644 --- a/components/licensor/ee/pkg/licensor/licensor.go +++ b/components/licensor/ee/pkg/licensor/licensor.go @@ -117,9 +117,18 @@ func (lvl LicenseLevel) allowance() allowance { return a } +// Fallback license is used when the instance exceeds the number of licenses - it allows limited access +var fallbackLicense = LicensePayload{ + ID: "fallback-license", + Level: LevelTeam, + Seats: 0, + // Domain, ValidUntil are free for all +} + +// Default license is used when no valid license is given - it allows full access up to 10 users var defaultLicense = LicensePayload{ ID: "default-license", - Level: LevelTeam, + Level: LevelEnterprise, Seats: 10, // Domain, ValidUntil are free for all } @@ -144,8 +153,9 @@ func matchesDomain(pattern, domain string) bool { // Evaluator determines what a license allows for type Evaluator struct { - invalid string - lic LicensePayload + invalid string + noFallback bool // Paid licenses cannot fallback and prevent additional signups + lic LicensePayload } // Validate returns false if the license isn't valid and a message explaining why that is. @@ -158,17 +168,27 @@ func (e *Evaluator) Validate() (msg string, valid bool) { } // Enabled determines if a feature is enabled by the license -func (e *Evaluator) Enabled(feature Feature) bool { +func (e *Evaluator) Enabled(feature Feature, seats int) bool { if e.invalid != "" { return false } - _, ok := e.lic.Level.allowance().Features[feature] + fmt.Println(e.noFallback) + + var ok bool + if e.hasEnoughSeats(seats) { + // License has enough seats available - evaluate this license + _, ok = e.lic.Level.allowance().Features[feature] + } else if !e.noFallback { + // License has run out of seats - use the fallback license + _, ok = fallbackLicense.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 { +// 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 } @@ -176,6 +196,19 @@ func (e *Evaluator) HasEnoughSeats(seats int) bool { return e.lic.Seats == 0 || seats <= e.lic.Seats } +// HasEnoughSeats is the public method to hasEnoughSeats. Will use fallback license if allowable +func (e *Evaluator) HasEnoughSeats(seats int) bool { + if e.invalid != "" { + return false + } + + if e.noFallback { + return e.hasEnoughSeats(seats) + } + // There is always more space if can use a fallback license + return true +} + // 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 diff --git a/components/licensor/ee/pkg/licensor/licensor_test.go b/components/licensor/ee/pkg/licensor/licensor_test.go index febeee139906f8..63f2211df6aa93 100644 --- a/components/licensor/ee/pkg/licensor/licensor_test.go +++ b/components/licensor/ee/pkg/licensor/licensor_test.go @@ -196,9 +196,9 @@ func TestSeats(t *testing.T) { ValidUntil: validUntil, }, Validate: func(t *testing.T, eval *Evaluator) { - withinLimits := eval.HasEnoughSeats(test.Probe) + withinLimits := eval.hasEnoughSeats(test.Probe) if withinLimits != test.WithinLimits { - t.Errorf("HasEnoughSeats did not behave as expected: lic=%d probe=%d expected=%v actual=%v", test.Licensed, test.Probe, test.WithinLimits, withinLimits) + t.Errorf("hasEnoughSeats did not behave as expected: lic=%d probe=%d expected=%v actual=%v", test.Licensed, test.Probe, test.WithinLimits, withinLimits) } }, Type: test.LicenseType, @@ -218,25 +218,50 @@ func TestFeatures(t *testing.T) { Level LicenseLevel Features []Feature LicenseType LicenseType + UserCount int }{ - {"Gitpod: no license", true, LicenseLevel(0), []Feature{FeaturePrebuild, FeatureAdminDashboard}, LicenseTypeGitpod}, - {"Gitpod: invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeGitpod}, - {"Gitpod: enterprise license", false, LevelEnterprise, []Feature{ + {"Gitpod (in seats): no license", true, LicenseLevel(0), []Feature{ FeatureAdminDashboard, FeatureSetTimeout, FeatureWorkspaceSharing, FeatureSnapshot, FeaturePrebuild, - }, LicenseTypeGitpod}, + }, LicenseTypeGitpod, 10}, + {"Gitpod (in seats): invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeGitpod, seats}, + {"Gitpod (in seats): enterprise license", false, LevelEnterprise, []Feature{ + FeatureAdminDashboard, + FeatureSetTimeout, + FeatureWorkspaceSharing, + FeatureSnapshot, + FeaturePrebuild, + }, LicenseTypeGitpod, seats}, + + {"Gitpod (over seats): no license", true, LicenseLevel(0), []Feature{ + FeatureAdminDashboard, + FeaturePrebuild, + }, LicenseTypeGitpod, 11}, + {"Gitpod (over seats): invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeGitpod, seats + 1}, + {"Gitpod (over seats): enterprise license", false, LevelEnterprise, []Feature{}, LicenseTypeGitpod, seats + 1}, - {"Replicated: invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeReplicated}, - {"Replicated: enterprise license", false, LevelEnterprise, []Feature{ + {"Replicated (in seats): invalid license level", false, LicenseLevel(666), []Feature{}, LicenseTypeReplicated, seats}, + {"Replicated (in seats): enterprise license", false, LevelEnterprise, []Feature{ FeatureAdminDashboard, FeatureSetTimeout, FeatureWorkspaceSharing, FeatureSnapshot, FeaturePrebuild, - }, LicenseTypeReplicated}, + }, LicenseTypeReplicated, seats}, + + // @todo(sje): add Replicated tests with no fallback + + {"Replicated (over seats - fallback): invalid license level", false, LicenseLevel(666), []Feature{ + FeatureAdminDashboard, + FeaturePrebuild, + }, LicenseTypeReplicated, seats + 1}, + {"Replicated (over seats - fallback): enterprise license", false, LevelEnterprise, []Feature{ + FeatureAdminDashboard, + FeaturePrebuild, + }, LicenseTypeReplicated, seats + 1}, } for _, test := range tests { @@ -261,13 +286,13 @@ func TestFeatures(t *testing.T) { for _, f := range test.Features { delete(unavailableFeatures, f) - if !eval.Enabled(f) { + if !eval.Enabled(f, test.UserCount) { t.Errorf("license does not enable %s, but should", f) } } for f := range unavailableFeatures { - if eval.Enabled(f) { + if eval.Enabled(f, test.UserCount) { t.Errorf("license not enables %s, but shouldn't", f) } } diff --git a/components/licensor/ee/pkg/licensor/replicated.go b/components/licensor/ee/pkg/licensor/replicated.go index 5a74b6eeefa43c..f706ca5fabf815 100644 --- a/components/licensor/ee/pkg/licensor/replicated.go +++ b/components/licensor/ee/pkg/licensor/replicated.go @@ -23,15 +23,25 @@ type replicatedFields struct { Value interface{} `json:"value"` // This is of type "fieldType" } +type replicatedLicenseType string + +// variable names are what Replicated calls them in the vendor portal +const ( + replicatedLicenseTypeCommunity replicatedLicenseType = "community" + replicatedLicenseTypeDevelopment replicatedLicenseType = "dev" + replicatedLicenseTypePaid replicatedLicenseType = "prod" + replicatedLicenseTypeTrial replicatedLicenseType = "trial" +) + // replicatedLicensePayload exists to convert the JSON structure to a LicensePayload type replicatedLicensePayload struct { - LicenseID string `json:"license_id"` - InstallationID string `json:"installation_id"` - Assignee string `json:"assignee"` - ReleaseChannel string `json:"release_channel"` - LicenseType string `json:"license_type"` - ExpirationTime *time.Time `json:"expiration_time,omitempty"` // Not set if license never expires - Fields []replicatedFields `json:"fields"` + LicenseID string `json:"license_id"` + InstallationID string `json:"installation_id"` + Assignee string `json:"assignee"` + ReleaseChannel string `json:"release_channel"` + LicenseType replicatedLicenseType `json:"license_type"` + ExpirationTime *time.Time `json:"expiration_time,omitempty"` // Not set if license never expires + Fields []replicatedFields `json:"fields"` } type ReplicatedEvaluator struct { @@ -120,7 +130,8 @@ func newReplicatedEvaluator(client *http.Client, domain string) (res *Evaluator) } return &Evaluator{ - lic: lic, + lic: lic, + noFallback: replicatedPayload.LicenseType == replicatedLicenseTypePaid, // Don't allow paid license to use the fallback license } } diff --git a/components/licensor/typescript/ee/main.go b/components/licensor/typescript/ee/main.go index 02bab5adbe989c..a5f81a117fc8ad 100644 --- a/components/licensor/typescript/ee/main.go +++ b/components/licensor/typescript/ee/main.go @@ -16,7 +16,7 @@ import ( var ( instances map[int]*licensor.Evaluator = make(map[int]*licensor.Evaluator) - nextID int = 1 + nextID int = 1 ) // Init initializes the global license evaluator from an environment variable @@ -49,13 +49,13 @@ func Validate(id int) (msg *C.char, valid bool) { // Enabled returns true if a license enables a feature //export Enabled -func Enabled(id int, feature *C.char) (enabled, ok bool) { +func Enabled(id int, feature *C.char, seats int) (enabled, ok bool) { e, ok := instances[id] if !ok { return } - return e.Enabled(licensor.Feature(C.GoString(feature))), true + return e.Enabled(licensor.Feature(C.GoString(feature)), seats), true } // HasEnoughSeats returns true if the license supports at least the given number of seats. diff --git a/components/licensor/typescript/ee/src/index.ts b/components/licensor/typescript/ee/src/index.ts index aa0350c348811a..d4b24bc083bb79 100644 --- a/components/licensor/typescript/ee/src/index.ts +++ b/components/licensor/typescript/ee/src/index.ts @@ -49,8 +49,8 @@ export class LicenseEvaluator { return { msg: v.msg, valid: false }; } - public isEnabled(feature: Feature): boolean { - return isEnabled(this.instanceID, feature); + public isEnabled(feature: Feature, seats: number): boolean { + return isEnabled(this.instanceID, feature, seats); } public hasEnoughSeats(seats: number): boolean { diff --git a/components/licensor/typescript/ee/src/module.cc b/components/licensor/typescript/ee/src/module.cc index 1fc61a1ef1f479..3787bcfc5bbbde 100644 --- a/components/licensor/typescript/ee/src/module.cc +++ b/components/licensor/typescript/ee/src/module.cc @@ -95,7 +95,7 @@ void EnabledM(const FunctionCallbackInfo &args) { Isolate *isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); - if (args.Length() < 2) { + if (args.Length() < 3) { isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "wrong number of arguments").ToLocalChecked())); return; } @@ -108,6 +108,10 @@ void EnabledM(const FunctionCallbackInfo &args) { isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "argument 1 must be a string").ToLocalChecked())); return; } + if (!args[2]->IsNumber() || args[2]->IsUndefined()) { + isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "argument 2 must be a number").ToLocalChecked())); + return; + } double rid = args[0]->NumberValue(context).FromMaybe(0); int id = static_cast(rid); @@ -116,8 +120,15 @@ void EnabledM(const FunctionCallbackInfo &args) { const char* cstr = ToCString(str); char* featurestr = const_cast(cstr); + double rseats = args[2]->NumberValue(context).FromMaybe(-1); + int seats = static_cast(rseats); + if (seats < 0) { + isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "cannot convert number of seats").ToLocalChecked())); + return; + } + // Call exported Go function, which returns a C string - Enabled_return r = Enabled(id, featurestr); + Enabled_return r = Enabled(id, featurestr, seats); if (!r.r1) { isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, "invalid instance ID").ToLocalChecked())); diff --git a/components/licensor/typescript/ee/src/nativemodule.d.ts b/components/licensor/typescript/ee/src/nativemodule.d.ts index 6fbdc47d658a4a..ae90944cbcbffe 100644 --- a/components/licensor/typescript/ee/src/nativemodule.d.ts +++ b/components/licensor/typescript/ee/src/nativemodule.d.ts @@ -9,7 +9,7 @@ export type Instance = number; export function init(key: string, domain: string): Instance; export function validate(id: Instance): { msg: string, valid: boolean }; -export function isEnabled(id: Instance, feature: Feature): boolean; +export function isEnabled(id: Instance, feature: Feature, seats: int): boolean; export function hasEnoughSeats(id: Instance, seats: int): boolean; export function inspect(id: Instance): string; export function dispose(id: Instance);