From a9f09cadc2d5f121cdefbc0b9c255d55c944f327 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:07:54 +0530 Subject: [PATCH] Premium SKU (#30396) * Added premium SKU * removed duplicate enterprise license check functions * Added license check on API layer * lint fix * lint fix * refactured signature: * test: Add comprehensive tests for license tier check functions * fixed test * text update * optimised license checks * fixedf test * Updated license valid function * webapp license checks * handling prekium SKU in webappp: * added plugin api method and general refactoring * Updated tests --------- Co-authored-by: Mattermost Build --- server/channels/api4/channel.go | 2 +- server/channels/api4/channel_test.go | 16 +- .../api4/custom_profile_attributes.go | 12 +- server/channels/api4/group.go | 2 +- server/channels/api4/handlers.go | 4 - server/channels/api4/ip_filtering.go | 4 +- .../api4/outgoing_oauth_connection.go | 2 +- server/channels/api4/post.go | 12 +- server/channels/app/notification.go | 2 +- server/channels/app/plugin.go | 11 +- server/channels/app/post_permission_utils.go | 10 +- server/channels/app/report.go | 2 +- .../scheduler.go | 3 +- server/config/client.go | 2 +- .../platform/services/telemetry/telemetry.go | 1 + server/public/model/license.go | 45 +++-- server/public/model/license_test.go | 186 ++++++++++++++++++ server/public/pluginapi/license.go | 25 +-- server/public/pluginapi/license_test.go | 122 ++++++++---- .../enterprise_edition_right_panel.tsx | 4 +- .../license_settings/license_settings.tsx | 4 +- .../system_users_export/index.tsx | 6 +- .../channel/details/index.ts | 10 +- .../workspace-optimization/dashboard.data.tsx | 8 +- .../configuration_bar.test.tsx | 6 +- .../channels/src/selectors/admin_console.jsx | 6 +- webapp/channels/src/utils/constants.tsx | 14 ++ .../channels/src/utils/license_utils.test.ts | 23 +-- webapp/channels/src/utils/license_utils.ts | 23 ++- webapp/channels/src/utils/subscription.ts | 5 +- 30 files changed, 409 insertions(+), 163 deletions(-) diff --git a/server/channels/api4/channel.go b/server/channels/api4/channel.go index da13890113..ef6063dac9 100644 --- a/server/channels/api4/channel.go +++ b/server/channels/api4/channel.go @@ -2453,7 +2453,7 @@ func convertGroupMessageToChannel(c *Context, w http.ResponseWriter, r *http.Req } func canEditChannelBanner(license *model.License, originalChannel *model.Channel) *model.AppError { - if license == nil || !license.IsE20OrEnterprise() { + if !model.MinimumPremiumLicense(license) { return model.NewAppError("", "license_error.feature_unavailable", nil, "feature is not available for the current license", http.StatusForbidden) } diff --git a/server/channels/api4/channel_test.go b/server/channels/api4/channel_test.go index 1860b2d294..a60a6fb195 100644 --- a/server/channels/api4/channel_test.go +++ b/server/channels/api4/channel_test.go @@ -810,7 +810,7 @@ func TestPatchChannel(t *testing.T) { t.Run("Should be able to configure channel banner on a channel", func(t *testing.T) { client.Logout(context.Background()) th.LoginBasic() - th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuPremium)) defer func() { th.App.Srv().RemoveLicense() }() @@ -845,7 +845,7 @@ func TestPatchChannel(t *testing.T) { t.Run("Cannot enable channel banner without configuring it", func(t *testing.T) { client.Logout(context.Background()) th.LoginBasic() - th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuPremium)) defer func() { th.App.Srv().RemoveLicense() }() @@ -905,7 +905,7 @@ func TestPatchChannel(t *testing.T) { t.Run("Cannot configure channel banner on a DM channel", func(t *testing.T) { client.Logout(context.Background()) th.LoginBasic() - th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuPremium)) defer func() { th.App.Srv().RemoveLicense() }() @@ -932,7 +932,7 @@ func TestPatchChannel(t *testing.T) { t.Run("Cannot configure channel banner on a GM channel", func(t *testing.T) { client.Logout(context.Background()) th.LoginBasic() - th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuPremium)) defer func() { th.App.Srv().RemoveLicense() }() @@ -5728,7 +5728,7 @@ func TestCanEditChannelBanner(t *testing.T) { }) t.Run("when channel type is direct message", func(t *testing.T) { - license := model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise) + license := model.NewTestLicenseSKU(model.LicenseShortSkuPremium) channel := &model.Channel{ Type: model.ChannelTypeDirect, } @@ -5741,7 +5741,7 @@ func TestCanEditChannelBanner(t *testing.T) { }) t.Run("when channel type is group message", func(t *testing.T) { - license := model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise) + license := model.NewTestLicenseSKU(model.LicenseShortSkuPremium) channel := &model.Channel{ Type: model.ChannelTypeGroup, } @@ -5753,7 +5753,7 @@ func TestCanEditChannelBanner(t *testing.T) { }) t.Run("when channel type is open and license is valid", func(t *testing.T) { - license := model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise) + license := model.NewTestLicenseSKU(model.LicenseShortSkuPremium) channel := &model.Channel{ Type: model.ChannelTypeOpen, } @@ -5763,7 +5763,7 @@ func TestCanEditChannelBanner(t *testing.T) { }) t.Run("when channel type is private and license is valid", func(t *testing.T) { - license := model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise) + license := model.NewTestLicenseSKU(model.LicenseShortSkuPremium) channel := &model.Channel{ Type: model.ChannelTypePrivate, } diff --git a/server/channels/api4/custom_profile_attributes.go b/server/channels/api4/custom_profile_attributes.go index c46f0bd82d..7076ecf1ac 100644 --- a/server/channels/api4/custom_profile_attributes.go +++ b/server/channels/api4/custom_profile_attributes.go @@ -27,7 +27,7 @@ func (api *API) InitCustomProfileAttributes() { } func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.listCPAFields", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } @@ -49,7 +49,7 @@ func createCPAField(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.createCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } @@ -89,7 +89,7 @@ func patchCPAField(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.patchCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } @@ -151,7 +151,7 @@ func deleteCPAField(c *Context, w http.ResponseWriter, r *http.Request) { return } - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.deleteCPAField", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } @@ -255,7 +255,7 @@ func sanitizePropertyValue(cpaField *model.CPAField, rawValue json.RawMessage) ( } func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.patchCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } @@ -327,7 +327,7 @@ func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) { } func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) { - if c.App.Channels().License() == nil || !c.App.Channels().License().IsE20OrEnterprise() { + if !model.MinimumEnterpriseLicense(c.App.Channels().License()) { c.Err = model.NewAppError("Api4.listCPAValues", "api.custom_profile_attributes.license_error", nil, "", http.StatusForbidden) return } diff --git a/server/channels/api4/group.go b/server/channels/api4/group.go index 34cc7ebaf7..ba34e0ebe0 100644 --- a/server/channels/api4/group.go +++ b/server/channels/api4/group.go @@ -1482,7 +1482,7 @@ func licensedAndConfiguredForGroupBySource(app *app.App, source model.GroupSourc return model.NewAppError("", "api.ldap_groups.license_error", nil, "", http.StatusForbidden) } - if source == model.GroupSourceCustom && lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise { + if source == model.GroupSourceCustom && !model.MinimumProfessionalLicense(lic) { return model.NewAppError("", "api.custom_groups.license_error", nil, "", http.StatusBadRequest) } diff --git a/server/channels/api4/handlers.go b/server/channels/api4/handlers.go index 85bf3505ca..32a96e74dc 100644 --- a/server/channels/api4/handlers.go +++ b/server/channels/api4/handlers.go @@ -238,10 +238,6 @@ func requireLicense(c *Context) *model.AppError { return nil } -func minimumProfessionalLicense(c *Context) *model.AppError { - return model.MinimumProfessionalProvidedLicense(c.App.Srv().License()) -} - func setHandlerOpts(handler *web.Handler, opts ...APIHandlerOption) { if len(opts) == 0 { return diff --git a/server/channels/api4/ip_filtering.go b/server/channels/api4/ip_filtering.go index 62ab0bab0f..a79689c5fa 100644 --- a/server/channels/api4/ip_filtering.go +++ b/server/channels/api4/ip_filtering.go @@ -21,7 +21,9 @@ func (api *API) InitIPFiltering() { } func ensureIPFilteringInterface(c *Context, where string) (einterfaces.IPFilteringInterface, bool) { - if c.App.IPFiltering() == nil || !c.App.Config().FeatureFlags.CloudIPFiltering || c.App.License() == nil || !c.App.License().IsCloud() || c.App.License().SkuShortName != model.LicenseShortSkuEnterprise { + license := c.App.License() + ipFilteringFeatureFlag := c.App.Config().FeatureFlags.CloudIPFiltering + if c.App.IPFiltering() == nil || !ipFilteringFeatureFlag || license == nil || !license.IsCloud() || !model.MinimumEnterpriseLicense(license) { c.Err = model.NewAppError(where, "api.context.ip_filtering.not_available.app_error", nil, "", http.StatusNotImplemented) return nil, false } diff --git a/server/channels/api4/outgoing_oauth_connection.go b/server/channels/api4/outgoing_oauth_connection.go index ca47610aaf..26aff65090 100644 --- a/server/channels/api4/outgoing_oauth_connection.go +++ b/server/channels/api4/outgoing_oauth_connection.go @@ -63,7 +63,7 @@ func ensureOutgoingOAuthConnectionInterface(c *Context, where string) (einterfac return nil, false } - if c.App.OutgoingOAuthConnections() == nil || c.App.License() == nil || c.App.License().SkuShortName != model.LicenseShortSkuEnterprise { + if c.App.OutgoingOAuthConnections() == nil || !model.MinimumEnterpriseLicense(c.App.License()) { c.Err = model.NewAppError(where, "api.license.upgrade_needed.app_error", nil, "", http.StatusNotImplemented) return nil, false } diff --git a/server/channels/api4/post.go b/server/channels/api4/post.go index 4e0a5c69cb..f45e25da30 100644 --- a/server/channels/api4/post.go +++ b/server/channels/api4/post.go @@ -1083,11 +1083,11 @@ func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) { func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) { // license check - permissionErr := minimumProfessionalLicense(c) - if permissionErr != nil { - c.Err = permissionErr + if !model.MinimumProfessionalLicense(c.App.Srv().License()) { + c.Err = model.NewAppError("", model.NoTranslation, nil, "feature is not available for the current license", http.StatusNotImplemented) return } + c.RequirePostId().RequireUserId() if c.Err != nil { return @@ -1122,11 +1122,11 @@ func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) { func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) { // license check - permissionErr := minimumProfessionalLicense(c) - if permissionErr != nil { - c.Err = permissionErr + if !model.MinimumProfessionalLicense(c.App.Srv().License()) { + c.Err = model.NewAppError("", "license_error.feature_unavailable", nil, "feature is not available for the current license", http.StatusNotImplemented) return } + c.RequirePostId().RequireUserId() if c.Err != nil { return diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index 1c4dd0e393..6a35a65fbf 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -1465,7 +1465,7 @@ func (a *App) allowChannelMentions(c request.CTX, post *model.Post, numProfiles // allowGroupMentions returns whether or not the group mentions are allowed for the given post. func (a *App) allowGroupMentions(c request.CTX, post *model.Post) bool { - if license := a.Srv().License(); license == nil || (license.SkuShortName != model.LicenseShortSkuProfessional && license.SkuShortName != model.LicenseShortSkuEnterprise) { + if !model.MinimumProfessionalLicense(a.Srv().License()) { return false } diff --git a/server/channels/app/plugin.go b/server/channels/app/plugin.go index 82c8411b0f..9141fea5ac 100644 --- a/server/channels/app/plugin.go +++ b/server/channels/app/plugin.go @@ -670,7 +670,6 @@ func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError) } - isEnterpriseLicense := a.License() != nil && a.License().IsE20OrEnterprise() for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() { if prepackaged.Manifest == nil { continue @@ -687,7 +686,7 @@ func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model // If not enterprise, check version. // Playbooks is not listed in the marketplace, this only handles prepackaged. - if !isEnterpriseLicense { + if !model.MinimumEnterpriseLicense(a.License()) { if prepackaged.Manifest.Id == model.PluginIdPlaybooks { version, err := semver.Parse(prepackaged.Manifest.Version) if err != nil { @@ -1033,8 +1032,8 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (* if err != nil { return nil, errors.Wrapf(err, "Unable to verify prepackaged playbooks version") } - license := ch.License() - hasEnterpriseLicense := license != nil && license.IsE20OrEnterprise() + + hasEnterpriseLicense := model.MinimumEnterpriseLicense(ch.License()) // Do not install playbooks >=v2 if we do not have an enterprise license if version.GTE(SemVerV2) && !hasEnterpriseLicense { @@ -1105,15 +1104,13 @@ func (ch *Channels) pluginIsTransitionallyPrepackaged(m *model.Manifest) bool { // - the server is not enterprise licensed // - the playbooks version is = ProfessionalTier +} + +// MinimumEnterpriseLicense returns true if the provided license is at least a enterprise license. +// Higher tier licenses also satisfy the condition. +func MinimumEnterpriseLicense(license *License) bool { + return license != nil && LicenseToLicenseTier[license.SkuShortName] >= EnterpriseTier +} + +// MinimumPremiumLicense returns true if the provided license is at least a premium license. +func MinimumPremiumLicense(license *License) bool { + return license != nil && LicenseToLicenseTier[license.SkuShortName] >= PremiumTier } diff --git a/server/public/model/license_test.go b/server/public/model/license_test.go index 900639e744..30fee220ff 100644 --- a/server/public/model/license_test.go +++ b/server/public/model/license_test.go @@ -485,3 +485,189 @@ func TestLicenseHasSharedChannels(t *testing.T) { }) } } + +func TestMinimumProfessionalLicense(t *testing.T) { + testCases := []struct { + description string + license *License + expectedValue bool + }{ + { + "nil license", + nil, + false, + }, + { + "professional license", + &License{ + SkuShortName: LicenseShortSkuProfessional, + }, + true, + }, + { + "enterprise license", + &License{ + SkuShortName: LicenseShortSkuEnterprise, + }, + true, + }, + { + "premium license", + &License{ + SkuShortName: LicenseShortSkuPremium, + }, + true, + }, + { + "E10 license", + &License{ + SkuShortName: LicenseShortSkuE10, + }, + false, + }, + { + "E20 license", + &License{ + SkuShortName: LicenseShortSkuE20, + }, + false, + }, + { + "unknown license", + &License{ + SkuShortName: "unknown", + }, + false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, MinimumProfessionalLicense(testCase.license)) + }) + } +} + +func TestMinimumEnterpriseLicense(t *testing.T) { + testCases := []struct { + description string + license *License + expectedValue bool + }{ + { + "nil license", + nil, + false, + }, + { + "professional license", + &License{ + SkuShortName: LicenseShortSkuProfessional, + }, + false, + }, + { + "enterprise license", + &License{ + SkuShortName: LicenseShortSkuEnterprise, + }, + true, + }, + { + "premium license", + &License{ + SkuShortName: LicenseShortSkuPremium, + }, + true, + }, + { + "E10 license", + &License{ + SkuShortName: LicenseShortSkuE10, + }, + false, + }, + { + "E20 license", + &License{ + SkuShortName: LicenseShortSkuE20, + }, + false, + }, + { + "unknown license", + &License{ + SkuShortName: "unknown", + }, + false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, MinimumEnterpriseLicense(testCase.license)) + }) + } +} + +func TestMinimumPremiumLicense(t *testing.T) { + testCases := []struct { + description string + license *License + expectedValue bool + }{ + { + "nil license", + nil, + false, + }, + { + "professional license", + &License{ + SkuShortName: LicenseShortSkuProfessional, + }, + false, + }, + { + "enterprise license", + &License{ + SkuShortName: LicenseShortSkuEnterprise, + }, + false, + }, + { + "premium license", + &License{ + SkuShortName: LicenseShortSkuPremium, + }, + true, + }, + { + "E10 license", + &License{ + SkuShortName: LicenseShortSkuE10, + }, + false, + }, + { + "E20 license", + &License{ + SkuShortName: LicenseShortSkuE20, + }, + false, + }, + { + "unknown license", + &License{ + SkuShortName: "unknown", + }, + false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.expectedValue, MinimumPremiumLicense(testCase.license)) + }) + } +} diff --git a/server/public/pluginapi/license.go b/server/public/pluginapi/license.go index f332280cef..a91ff4b38f 100644 --- a/server/public/pluginapi/license.go +++ b/server/public/pluginapi/license.go @@ -4,13 +4,6 @@ import ( "github.com/mattermost/mattermost/server/public/model" ) -const ( - e10 = "E10" - e20 = "E20" - professional = "professional" - enterprise = "enterprise" -) - // IsEnterpriseLicensedOrDevelopment returns true when the server is licensed with any Mattermost // Enterprise License, or has `EnableDeveloper` and `EnableTesting` configuration settings // enabled signaling a non-production, developer mode. @@ -30,7 +23,7 @@ func isValidSkuShortName(license *model.License) bool { } switch license.SkuShortName { - case e10, e20, professional, enterprise: + case model.LicenseShortSkuE10, model.LicenseShortSkuE20, model.LicenseShortSkuProfessional, model.LicenseShortSkuEnterprise, model.LicenseShortSkuPremium: return true default: return false @@ -41,9 +34,7 @@ func isValidSkuShortName(license *model.License) bool { // Enterprise E10 License or a Mattermost Professional License, or has `EnableDeveloper` and // `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bool { - if license != nil && - (license.SkuShortName == e10 || license.SkuShortName == professional || - license.SkuShortName == e20 || license.SkuShortName == enterprise) { + if model.MinimumProfessionalLicense(license) { return true } @@ -65,7 +56,7 @@ func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bo // Enterprise E20 License or a Mattermost Enterprise License, or has `EnableDeveloper` and // `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bool { - if license != nil && (license.SkuShortName == e20 || license.SkuShortName == enterprise) { + if model.MinimumEnterpriseLicense(license) { return true } @@ -83,6 +74,16 @@ func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bo return IsConfiguredForDevelopment(config) } +// IsPremiumLicensedOrDevelopment returns true when the server is licensed with a Mattermost +// Premium License, or has `EnableDeveloper` and `EnableTesting` configuration settings +func IsPremiumLicensedOrDevelopment(config *model.Config, license *model.License) bool { + if license != nil && license.SkuShortName == model.LicenseShortSkuPremium { + return true + } + + return IsConfiguredForDevelopment(config) +} + // IsConfiguredForDevelopment returns true when the server has `EnableDeveloper` and `EnableTesting` // configuration settings enabled, signaling a non-production, developer mode. func IsConfiguredForDevelopment(config *model.Config) bool { diff --git a/server/public/pluginapi/license_test.go b/server/public/pluginapi/license_test.go index 55835e9711..96162d2c75 100644 --- a/server/public/pluginapi/license_test.go +++ b/server/public/pluginapi/license_test.go @@ -133,19 +133,6 @@ func TestIsE20LicensedOrDevelopment(t *testing.T) { Features: &model.Features{FutureFeatures: bToP(true)}, })) }) - t.Run("license with E20 SKU name, disabled future features", func(t *testing.T) { - assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E20", - Features: &model.Features{FutureFeatures: bToP(false)}, - })) - }) - - t.Run("license with E20 SKU name, enabled future features", func(t *testing.T) { - assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E20", - Features: &model.Features{FutureFeatures: bToP(true)}, - })) - }) t.Run("license with enterprise SKU name, disabled future features", func(t *testing.T) { assert.True(t, IsE20LicensedOrDevelopment(nil, &model.License{ @@ -229,20 +216,6 @@ func TestIsE10LicensedOrDevelopment(t *testing.T) { )) }) - t.Run("license with E10 SKU name, disabled LDAP", func(t *testing.T) { - assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E10", - Features: &model.Features{LDAP: bToP(false)}, - })) - }) - - t.Run("license with E10 SKU name, enabled LDAP", func(t *testing.T) { - assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E10", - Features: &model.Features{LDAP: bToP(true)}, - })) - }) - t.Run("license with professional SKU name, disabled LDAP", func(t *testing.T) { assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ SkuShortName: "professional", @@ -256,19 +229,6 @@ func TestIsE10LicensedOrDevelopment(t *testing.T) { Features: &model.Features{LDAP: bToP(true)}, })) }) - t.Run("license with E20 SKU name, disabled LDAP", func(t *testing.T) { - assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E20", - Features: &model.Features{LDAP: bToP(false)}, - })) - }) - - t.Run("license with E20 SKU name, enabled LDAP", func(t *testing.T) { - assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ - SkuShortName: "E20", - Features: &model.Features{LDAP: bToP(true)}, - })) - }) t.Run("license with enterprise SKU name, disabled LDAP", func(t *testing.T) { assert.True(t, IsE10LicensedOrDevelopment(nil, &model.License{ @@ -325,6 +285,88 @@ func TestIsValidSKUShortName(t *testing.T) { }) } +func TestIsPremiumOrDevelopment(t *testing.T) { + t.Run("nil license features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{})) + }) + + t.Run("nil future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{Features: &model.Features{}})) + }) + + t.Run("disabled future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + FutureFeatures: bToP(false), + }})) + }) + + t.Run("should have no affect of future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{Features: &model.Features{ + FutureFeatures: bToP(true), + }})) + }) + + t.Run("no license, no config", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, nil)) + }) + + t.Run("no license, nil config", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: nil, EnableTesting: nil}}, + nil, + )) + }) + + t.Run("no license, only developer mode", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(false)}}, + nil, + )) + }) + + t.Run("no license, only testing mode", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(false), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("no license, developer and testing mode", func(t *testing.T) { + assert.True(t, IsPremiumLicensedOrDevelopment( + &model.Config{ServiceSettings: model.ServiceSettings{EnableDeveloper: bToP(true), EnableTesting: bToP(true)}}, + nil, + )) + }) + + t.Run("license with E10 SKU name, disabled future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with E10 SKU name, enabled future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E10", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) + + t.Run("license with E20 SKU name, disabled future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{FutureFeatures: bToP(false)}, + })) + }) + + t.Run("license with E20 SKU name, enabled future features", func(t *testing.T) { + assert.False(t, IsPremiumLicensedOrDevelopment(nil, &model.License{ + SkuShortName: "E20", + Features: &model.Features{FutureFeatures: bToP(true)}, + })) + }) +} + func bToP(b bool) *bool { return &b } diff --git a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel.tsx b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel.tsx index 9e94a9acef..be08af9b4d 100644 --- a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_right_panel.tsx @@ -9,7 +9,7 @@ import type {ClientLicense} from '@mattermost/types/config'; import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us'; import SetupSystemSvg from 'components/common/svg_images_components/setup_system'; -import {isEnterpriseOrE20License} from 'utils/license_utils'; +import {isEnterpriseLicense} from 'utils/license_utils'; export interface EnterpriseEditionProps { isTrialLicense: boolean; @@ -29,7 +29,7 @@ const EnterpriseEditionRightPanel = ({ 'And more...', ]; - const isEnterpriseOrE20 = isEnterpriseOrE20License(license); + const isEnterpriseOrE20 = isEnterpriseLicense(license); const contactSalesBtn = (
diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx index d57a26911f..b3aac51e8a 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx @@ -18,7 +18,7 @@ import ExternalLink from 'components/external_link'; import AdminHeader from 'components/widgets/admin_console/admin_header'; import {AboutLinks, CloudLinks, ModalIdentifiers} from 'utils/constants'; -import {isLicenseExpired, isLicenseExpiring, isTrialLicense, isEnterpriseOrE20License, licenseSKUWithFirstLetterCapitalized} from 'utils/license_utils'; +import {isLicenseExpired, isLicenseExpiring, isTrialLicense, licenseSKUWithFirstLetterCapitalized, isEnterpriseLicense} from 'utils/license_utils'; import type {ModalData} from 'types/actions'; @@ -384,7 +384,7 @@ export default class LicenseSettings extends React.PureComponent {
{rightPanel}
- {!isEnterpriseOrE20License(license) && this.comparePlans} + {!isEnterpriseLicense(license) && this.comparePlans}
diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_export/index.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_export/index.tsx index 9727afe9f2..d89b0a746a 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users_export/index.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_export/index.tsx @@ -20,13 +20,15 @@ import {getAdminConsoleUserManagementTableProperties} from 'selectors/views/admi import WithTooltip from 'components/with_tooltip'; -import {LicenseSkus, ModalIdentifiers} from 'utils/constants'; +import {ModalIdentifiers} from 'utils/constants'; +import {isMinimumProfessionalLicense} from 'utils/license_utils'; import {ExportErrorModal} from './export_error_modal'; import {ExportUserDataModal} from './export_user_data_modal'; import {UpgradeExportDataModal} from './upgrade_export_data_modal'; import {convertTableOptionsToUserReportOptions} from '../utils'; + import './system_users_export.scss'; interface Props { @@ -47,7 +49,7 @@ export function SystemUsersExport(props: Props) { } const license = useSelector(getLicense); - const isLicensed = license.IsLicensed === 'true' && (license.SkuShortName === LicenseSkus.Professional || license.SkuShortName === LicenseSkus.Enterprise); + const isLicensed = license.IsLicensed === 'true' && isMinimumProfessionalLicense(license); async function doExport(checked?: boolean) { const {error} = await dispatch(startUsersBatchExport(tableOptionsToUserReport)); diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/index.ts b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/index.ts index 76f9a89337..2d89a0ba91 100644 --- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/index.ts +++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/index.ts @@ -36,7 +36,7 @@ import {getTeam} from 'mattermost-redux/selectors/entities/teams'; import {setNavigationBlocked} from 'actions/admin_actions'; -import {LicenseSkus} from 'utils/constants'; +import {isMinimumEnterpriseLicense, isMinimumProfessionalLicense} from 'utils/license_utils'; import ChannelDetails from './channel_details'; @@ -54,11 +54,11 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const isLicensed = license?.IsLicensed === 'true'; - // Channel Moderation is only available for Professional, Enterprise and backward compatible with E20 - const channelModerationEnabled = isLicensed && (license.SkuShortName === LicenseSkus.Professional || license.SkuShortName === LicenseSkus.Enterprise || license.SkuShortName === LicenseSkus.E20); + // Channel Moderation is only available for Professional and above + const channelModerationEnabled = isLicensed && isMinimumProfessionalLicense(license); - // Channel Groups is only available for Enterprise and backward compatible with E20 - const channelGroupsEnabled = isLicensed && (license.SkuShortName === LicenseSkus.Enterprise || license.SkuShortName === LicenseSkus.E20); + // Channel Groups is only available for Enterprise and above + const channelGroupsEnabled = isLicensed && isMinimumEnterpriseLicense(license); const guestAccountsEnabled = config.EnableGuestAccounts === 'true'; const channelID = ownProps.match.params.channel_id; diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx index b0b6aabd04..1d3369bec3 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx @@ -31,7 +31,7 @@ import {runEaseOfUseChecks} from './dashboard_checks/easy_management'; import {runPerformanceChecks} from './dashboard_checks/performance'; import {runUpdateChecks} from './dashboard_checks/updates'; -import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; +import {daysToLicenseExpire, getIsStarterLicense, isEnterpriseLicense} from '../../../utils/license_utils'; export const impactModifiers: Record = { [ItemStatus.NONE]: 1, @@ -232,7 +232,7 @@ const useMetricsData = ( const isLicensed = license?.IsLicensed === 'true' && daysUntilExpiration >= 0; const isCloud = license?.Cloud === 'true'; - const isEnterpriseLicense = isEnterpriseOrE20License(license); + const isEnterprise = isEnterpriseLicense(license); const isStarterLicense = getIsStarterLicense(license); const [, contactSalesLink] = useOpenSalesLink(); @@ -244,13 +244,13 @@ const useMetricsData = ( const options: Options = useMemo(() => ({ isLicensed, - isEnterpriseLicense, + isEnterpriseLicense: isEnterprise, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion, - }), [isLicensed, isEnterpriseLicense, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]); + }), [isLicensed, isEnterprise, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]); useEffect(() => { setLoading(true); diff --git a/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.test.tsx b/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.test.tsx index 3553fafa3e..0b1f72d93b 100644 --- a/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.test.tsx +++ b/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.test.tsx @@ -34,7 +34,7 @@ describe('components/ConfigurationBar', () => { }; test('should match snapshot, expired, in grace period', () => { - const props = {...baseProps, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - millisPerDay}}; + const props = {...baseProps, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - millisPerDay, SkuShortName: 'enterprise'}}; const wrapper = shallowWithIntl( , ); @@ -43,7 +43,7 @@ describe('components/ConfigurationBar', () => { }); test('should match snapshot, expired', () => { - const props = {...baseProps, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - (11 * millisPerDay)}}; + const props = {...baseProps, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - (11 * millisPerDay), SkuShortName: 'enterprise'}}; const wrapper = shallowWithIntl( , ); @@ -52,7 +52,7 @@ describe('components/ConfigurationBar', () => { }); test('should match snapshot, expired, regular user', () => { - const props = {...baseProps, canViewSystemErrors: false, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - (11 * millisPerDay)}}; + const props = {...baseProps, canViewSystemErrors: false, license: {Id: '1234', IsLicensed: 'true', ExpiresAt: Date.now() - (11 * millisPerDay), SkuShortName: 'enterprise'}}; const wrapper = shallowWithIntl( , ); diff --git a/webapp/channels/src/selectors/admin_console.jsx b/webapp/channels/src/selectors/admin_console.jsx index 2e997f4084..c7ca344273 100644 --- a/webapp/channels/src/selectors/admin_console.jsx +++ b/webapp/channels/src/selectors/admin_console.jsx @@ -11,7 +11,7 @@ import {getMySystemPermissions, haveISystemPermission} from 'mattermost-redux/se import AdminDefinition from 'components/admin_console/admin_definition'; -import {isEnterpriseOrE20License} from '../utils/license_utils'; +import {isEnterpriseLicense} from '../utils/license_utils'; export const getAdminDefinition = createSelector( 'getAdminDefinition', @@ -64,7 +64,7 @@ export const getShowManageUserSettings = createSelector( (license, state) => { const hasWriteUserManagementPermission = haveISystemPermission(state, {permission: Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_USERS}); - const isEnterprise = isEnterpriseOrE20License(license); + const isEnterprise = isEnterpriseLicense(license); return hasWriteUserManagementPermission && isEnterprise; }, @@ -77,7 +77,7 @@ export const getShowLockedManageUserSettings = createSelector( (license, state) => { const hasWriteUserManagementPermission = haveISystemPermission(state, {permission: Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_USERS}); - const isEnterprise = isEnterpriseOrE20License(license); + const isEnterprise = isEnterpriseLicense(license); return hasWriteUserManagementPermission && !isEnterprise; }, diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 0cbde004d9..ed9b10ad07 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -539,6 +539,20 @@ export enum LicenseSkus { Starter = 'starter', Professional = 'professional', Enterprise = 'enterprise', + Premium = 'premium', +} + +export function getLicenseTier(licenseSku: string): number { + switch (licenseSku) { + case LicenseSkus.Professional: + return 10; + case LicenseSkus.Enterprise: + return 20; + case LicenseSkus.Premium: + return 30; + default: + return 0; + } } export const CloudProductToSku = { diff --git a/webapp/channels/src/utils/license_utils.test.ts b/webapp/channels/src/utils/license_utils.test.ts index 62c9d84024..f92d15252f 100644 --- a/webapp/channels/src/utils/license_utils.test.ts +++ b/webapp/channels/src/utils/license_utils.test.ts @@ -1,8 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {LicenseSkus} from 'utils/constants'; -import {isLicenseExpired, isLicenseExpiring, isLicensePastGracePeriod, isEnterpriseOrE20License} from 'utils/license_utils'; +import {isLicenseExpired, isLicenseExpiring, isLicensePastGracePeriod} from 'utils/license_utils'; describe('license_utils', () => { const millisPerDay = 24 * 60 * 60 * 1000; @@ -46,24 +45,4 @@ describe('license_utils', () => { expect(isLicensePastGracePeriod(license)).toBeTruthy(); }); }); - - describe('isEnterpriseOrE20License', () => { - it('should return False if not Enterprise or E20', () => { - const license = {Id: '1234', IsLicensed: 'true', Cloud: 'false', SkuShortName: LicenseSkus.Starter}; - - expect(isEnterpriseOrE20License(license)).toBeFalsy(); - }); - - it('should return True if Enterprise', () => { - const license = {Id: '1234', IsLicensed: 'true', Cloud: 'false', SkuShortName: LicenseSkus.Enterprise}; - - expect(isEnterpriseOrE20License(license)).toBeTruthy(); - }); - - it('should return True if E20', () => { - const license = {Id: '1234', IsLicensed: 'true', Cloud: 'false', SkuShortName: LicenseSkus.E20}; - - expect(isEnterpriseOrE20License(license)).toBeTruthy(); - }); - }); }); diff --git a/webapp/channels/src/utils/license_utils.ts b/webapp/channels/src/utils/license_utils.ts index 7d89e29bcc..1bc1d8b7f0 100644 --- a/webapp/channels/src/utils/license_utils.ts +++ b/webapp/channels/src/utils/license_utils.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import type {Product} from '@mattermost/types/cloud'; import type {ClientLicense} from '@mattermost/types/config'; -import {CloudProducts, LicenseSkus, SelfHostedProducts} from 'utils/constants'; +import {CloudProducts, getLicenseTier, LicenseSkus, SelfHostedProducts} from 'utils/constants'; const LICENSE_EXPIRY_NOTIFICATION = 1000 * 60 * 60 * 24 * 60; // 60 days const LICENSE_GRACE_PERIOD = 1000 * 60 * 60 * 24 * 10; // 10 days @@ -84,14 +84,11 @@ export function getIsGovSku(license: ClientLicense) { return license?.IsGovSku === 'true'; } -export function isEnterpriseOrE20License(license: ClientLicense) { - return license?.SkuShortName === LicenseSkus.Enterprise || license?.SkuShortName === LicenseSkus.E20; -} - export const isEnterpriseLicense = (license?: ClientLicense) => { switch (license?.SkuShortName) { case LicenseSkus.Enterprise: case LicenseSkus.E20: + case LicenseSkus.Premium: return true; } @@ -115,3 +112,19 @@ export function isEnterpriseOrCloudOrSKUStarterFree(license: ClientLicense, subs return isCloudStarterFree || isSelfHostedStarter || isStarterSKULicense; } + +export function isMinimumProfessionalLicense(license: ClientLicense): boolean { + if (!license) { + return false; + } + + return getLicenseTier(license.SkuShortName) >= getLicenseTier(LicenseSkus.Professional); +} + +export function isMinimumEnterpriseLicense(license: ClientLicense): boolean { + if (!license) { + return false; + } + + return getLicenseTier(license.SkuShortName) >= getLicenseTier(LicenseSkus.Enterprise); +} diff --git a/webapp/channels/src/utils/subscription.ts b/webapp/channels/src/utils/subscription.ts index 20761f164e..7eed993339 100644 --- a/webapp/channels/src/utils/subscription.ts +++ b/webapp/channels/src/utils/subscription.ts @@ -18,9 +18,12 @@ export const getSkuDisplayName = (skuShortName: string, isGovSku: boolean): stri case LicenseSkus.Starter: skuName = 'Starter'; break; - default: + case LicenseSkus.Enterprise: skuName = 'Enterprise'; break; + default: + skuName = 'Premium'; + break; } skuName += isGovSku ? ' Gov' : '';