From dbcd904cc23c32dde5e0c92f499f0cbd7ec7ff91 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Tue, 23 Jun 2026 15:26:28 -0300 Subject: [PATCH] [MM-69229] Promote CloudIPFiltering: remove the feature flag (#37150) The CloudIPFiltering flag (default false, shipped v9.4) gated the cloud-only IP filtering admin feature. The feature stays cloud-only independent of the flag: - Server: ensureIPFilteringInterface still requires license.IsCloud() and MinimumEnterpriseLicense, so non-cloud servers continue to get 501 Not Implemented. Added not-cloud coverage to applyIPFilters and getMyIP to match the existing getIPFilters case. - Webapp: the admin console section now gates on a Cloud license + Enterprise tier (it.licensedForFeature('Cloud')) instead of the flag, preserving cloud-only visibility. Drops the struct field, its SetDefaults entry, the e2e default_config mirror, the admin_sidebar test fixtures, and the flag-toggling test cases. --- .../lib/src/server/default_config.ts | 1 - server/channels/api4/ip_filtering.go | 3 +- server/channels/api4/ip_filtering_test.go | 72 ++++++++----------- server/public/model/feature_flags.go | 5 +- .../admin_console/admin_definition.tsx | 2 +- .../admin_sidebar/admin_sidebar.test.tsx | 2 - 6 files changed, 31 insertions(+), 54 deletions(-) diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts index d572ca09543..a75f07a39fe 100644 --- a/e2e-tests/playwright/lib/src/server/default_config.ts +++ b/e2e-tests/playwright/lib/src/server/default_config.ts @@ -767,7 +767,6 @@ const defaultServerConfig: AdminConfig = { EnableExportDirectDownload: false, MoveThreadsEnabled: false, StreamlinedMarketplace: true, - CloudIPFiltering: false, ConsumePostHook: false, CloudAnnualRenewals: false, CloudDedicatedExportUI: false, diff --git a/server/channels/api4/ip_filtering.go b/server/channels/api4/ip_filtering.go index c44e75a8a87..1dcb458eaf2 100644 --- a/server/channels/api4/ip_filtering.go +++ b/server/channels/api4/ip_filtering.go @@ -21,8 +21,7 @@ func (api *API) InitIPFiltering() { func ensureIPFilteringInterface(c *Context, where string) (einterfaces.IPFilteringInterface, bool) { license := c.App.License() - ipFilteringFeatureFlag := c.App.Config().FeatureFlags.CloudIPFiltering - if c.App.IPFiltering() == nil || !ipFilteringFeatureFlag || license == nil || !license.IsCloud() || !model.MinimumEnterpriseLicense(license) { + if c.App.IPFiltering() == nil || 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/ip_filtering_test.go b/server/channels/api4/ip_filtering_test.go index d0fdca8ae02..f6d74924063 100644 --- a/server/channels/api4/ip_filtering_test.go +++ b/server/channels/api4/ip_filtering_test.go @@ -4,7 +4,6 @@ package api4 import ( "context" - "os" "testing" "github.com/mattermost/mattermost/server/public/model" @@ -30,7 +29,6 @@ func Test_getIPFilters(t *testing.T) { } t.Run("No license returns 501", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -48,27 +46,7 @@ func Test_getIPFilters(t *testing.T) { require.Equal(t, 501, r.StatusCode) }) - t.Run("No feature flag returns 501", func(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false") - defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING") - th := Setup(t).InitBasic(t) - - ipFiltering := &mocks.IPFilteringInterface{} - th.App.Srv().IPFiltering = ipFiltering - - th.App.Srv().SetLicense(lic) - - _, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) - require.NoError(t, err) - - ipFilters, r, err := th.Client.GetIPFilters(context.Background()) - require.Error(t, err) - require.Nil(t, ipFilters) - require.Equal(t, 501, r.StatusCode) - }) - - t.Run("Feature flag and license but no permission", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") + t.Run("License but no permission", func(t *testing.T) { th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -85,8 +63,7 @@ func Test_getIPFilters(t *testing.T) { require.Equal(t, 403, r.StatusCode) }) - t.Run("Feature flag and license and permission", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") + t.Run("License and permission", func(t *testing.T) { th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -109,8 +86,7 @@ func Test_getIPFilters(t *testing.T) { require.Equal(t, 200, r.StatusCode) }) - t.Run("Feature flag and license and permission but not cloud returns 503", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") + t.Run("License and permission but not cloud returns 501", func(t *testing.T) { th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -149,7 +125,6 @@ func Test_applyIPFilters(t *testing.T) { // Initialize the allowedRanges variable t.Run("No license returns 501", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -167,16 +142,14 @@ func Test_applyIPFilters(t *testing.T) { require.Equal(t, 501, r.StatusCode) }) - t.Run("License but no feature flag returns 501", func(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false") - defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING") + t.Run("License and permission but not cloud returns 501", func(t *testing.T) { th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} th.App.Srv().IPFiltering = ipFiltering - th.App.Srv().SetLicense(lic) + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) - _, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) + _, _, err := th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password) require.NoError(t, err) ipFilters, r, err := th.Client.ApplyIPFilters(context.Background(), allowedRanges) @@ -185,8 +158,7 @@ func Test_applyIPFilters(t *testing.T) { require.Equal(t, 501, r.StatusCode) }) - t.Run("feature flag and license but no permission", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") + t.Run("License but no permission", func(t *testing.T) { th := Setup(t).InitBasic(t) _, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) @@ -202,8 +174,7 @@ func Test_applyIPFilters(t *testing.T) { require.Equal(t, 403, r.StatusCode) }) - t.Run("Feature flag and license and permission", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") + t.Run("License and permission", func(t *testing.T) { th := Setup(t).InitBasic(t) th.App.Srv().SetLicense(lic) @@ -250,7 +221,6 @@ func Test_getMyIP(t *testing.T) { ExpiresAt: model.GetMillis() + 100000, } t.Run("No license returns 501", func(t *testing.T) { - t.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true") th := Setup(t).InitBasic(t) ipFiltering := &mocks.IPFilteringInterface{} @@ -268,9 +238,23 @@ func Test_getMyIP(t *testing.T) { require.Equal(t, 501, r.StatusCode) }) - t.Run("Licensed, but no feature flag returns 501", func(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false") - defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING") + t.Run("Licensed but not cloud returns 501", func(t *testing.T) { + th := Setup(t).InitBasic(t) + + _, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) + require.NoError(t, err) + + ipFiltering := &mocks.IPFilteringInterface{} + th.App.Srv().IPFiltering = ipFiltering + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise)) + + myIP, r, err := th.Client.GetMyIP(context.Background()) + require.Error(t, err) + require.Nil(t, myIP) + require.Equal(t, 501, r.StatusCode) + }) + + t.Run("Licensed and cloud returns 200", func(t *testing.T) { th := Setup(t).InitBasic(t) _, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password) @@ -281,8 +265,8 @@ func Test_getMyIP(t *testing.T) { th.App.Srv().SetLicense(lic) myIP, r, err := th.Client.GetMyIP(context.Background()) - require.Error(t, err) - require.Nil(t, myIP) - require.Equal(t, 501, r.StatusCode) + require.NoError(t, err) + require.NotNil(t, myIP) + require.Equal(t, 200, r.StatusCode) }) } diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index bdb115510c3..0d22cf67159 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -47,9 +47,7 @@ type FeatureFlags struct { StreamlinedMarketplace bool - CloudIPFiltering bool - ConsumePostHook bool - + ConsumePostHook bool CloudAnnualRenewals bool CloudDedicatedExportUI bool @@ -172,7 +170,6 @@ func (f *FeatureFlags) SetDefaults() { f.EnableExportDirectDownload = false f.MoveThreadsEnabled = false f.StreamlinedMarketplace = true - f.CloudIPFiltering = false f.ConsumePostHook = false f.CloudAnnualRenewals = false f.CloudDedicatedExportUI = false diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index ce5a026d701..6108d2801a6 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -4294,7 +4294,7 @@ const AdminDefinition: AdminDefinitionType = { ip_filtering: { url: 'site_config/ip_filtering', title: adminDefinitionMessages.ip_filtering_title, - isHidden: it.not(it.all(it.configIsTrue('FeatureFlags', 'CloudIPFiltering'), it.minLicenseTier(LicenseSkus.Enterprise))), + isHidden: it.not(it.all(it.licensedForFeature('Cloud'), it.minLicenseTier(LicenseSkus.Enterprise))), isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.IP_FILTERING)), searchableStrings: [adminDefinitionMessages.ip_filtering_title], schema: { diff --git a/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx b/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx index 0bf8b1baa2c..4a10002cd5d 100644 --- a/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx +++ b/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx @@ -401,7 +401,6 @@ describe('components/AdminSidebar', () => { AttributeBasedAccessControl: true, CustomProfileAttributes: true, CloudDedicatedExportUI: true, - CloudIPFiltering: true, ExperimentalAuditSettingsSystemConsoleUI: true, }, }, @@ -537,7 +536,6 @@ describe('components/AdminSidebar', () => { AttributeBasedAccessControl: true, CustomProfileAttributes: true, CloudDedicatedExportUI: true, - CloudIPFiltering: true, ExperimentalAuditSettingsSystemConsoleUI: true, }, },