mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-19 02:06:37 +08:00
Mm 68282 admin ephemeral mode (#36194)
* adds feature flag to enable mattermost ephemeral mode * add ephemeral mode config settings to system console When feature flag is set to true a new section for Mobile Ephemeral Mode settings shows under the Mobile Security section in case a valid Enterprise Advanced License is active. * adds Mobile Ephemeral Mode settings playwright tests * improve descriptions for settings * improves error messages and hints * move validation to common helper and add new tests * reverts package-lock.json changes * proper struct alignment * proper message sorting in json file * use generic doc url for MEM section while docs are not ready * Proper formatting for playwright tests * fixes test
This commit is contained in:
@@ -790,6 +790,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
IntegratedBoards: false,
|
||||
CJKSearch: false,
|
||||
ManagedChannelCategories: false,
|
||||
MobileEphemeralMode: true,
|
||||
},
|
||||
ImportSettings: {
|
||||
Directory: './import',
|
||||
@@ -868,4 +869,10 @@ const defaultServerConfig: AdminConfig = {
|
||||
LLMServiceID: '',
|
||||
},
|
||||
},
|
||||
MobileEphemeralModeSettings: {
|
||||
Enable: false,
|
||||
DisconnectionTimeoutSeconds: 60,
|
||||
OfflinePersistenceTimerHours: 24,
|
||||
AutoCacheCleanupDays: 7,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,7 +54,13 @@ import BurnOnReadTimerChip from './channels/burn_on_read_timer_chip';
|
||||
import BurnOnReadConcealedPlaceholder from './channels/burn_on_read_concealed_placeholder';
|
||||
import BurnOnReadConfirmationModal from './channels/burn_on_read_confirmation_modal';
|
||||
// System Console Components
|
||||
import {AdminSectionPanel, DropdownSetting, RadioSetting, TextInputSetting} from './system_console/base_components';
|
||||
import {
|
||||
AdminSectionPanel,
|
||||
DropdownSetting,
|
||||
NumberInputSetting,
|
||||
RadioSetting,
|
||||
TextInputSetting,
|
||||
} from './system_console/base_components';
|
||||
import DelegatedGranularAdministration from './system_console/sections/user_management/delegated_granular_administration';
|
||||
import UserDetail from './system_console/sections/user_management/user_detail';
|
||||
import EditionAndLicense from './system_console/sections/about/edition_and_license';
|
||||
@@ -132,6 +138,7 @@ const components = {
|
||||
EditionAndLicense,
|
||||
MobileSecurity,
|
||||
Notifications,
|
||||
NumberInputSetting,
|
||||
RadioSetting,
|
||||
UsersAndTeams,
|
||||
SystemConsoleFeatureDiscovery,
|
||||
@@ -210,6 +217,7 @@ export {
|
||||
EditionAndLicense,
|
||||
MobileSecurity,
|
||||
Notifications,
|
||||
NumberInputSetting,
|
||||
RadioSetting,
|
||||
UsersAndTeams,
|
||||
SystemConsoleFeatureDiscovery,
|
||||
|
||||
@@ -94,6 +94,40 @@ export class TextInputSetting {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number Input Setting - represents a number input field
|
||||
* Uses getByRole('spinbutton') since <input type="number"> has ARIA role spinbutton
|
||||
*/
|
||||
export class NumberInputSetting {
|
||||
readonly container: Locator;
|
||||
readonly label: Locator;
|
||||
readonly input: Locator;
|
||||
readonly helpText: Locator;
|
||||
|
||||
constructor(container: Locator, labelText: string) {
|
||||
this.container = container;
|
||||
this.label = container.getByText(labelText);
|
||||
this.input = container.getByRole('spinbutton');
|
||||
this.helpText = container.locator('.help-text');
|
||||
}
|
||||
|
||||
async fill(value: string) {
|
||||
await this.input.fill(value);
|
||||
}
|
||||
|
||||
async getValue(): Promise<string> {
|
||||
return (await this.input.inputValue()) ?? '';
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.input.clear();
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdown Setting - represents a select dropdown
|
||||
*/
|
||||
|
||||
+52
-1
@@ -3,7 +3,13 @@
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import {RadioSetting, TextInputSetting, DropdownSetting, AdminSectionPanel} from '../../base_components';
|
||||
import {
|
||||
RadioSetting,
|
||||
TextInputSetting,
|
||||
NumberInputSetting,
|
||||
DropdownSetting,
|
||||
AdminSectionPanel,
|
||||
} from '../../base_components';
|
||||
|
||||
/**
|
||||
* System Console -> Environment -> Mobile Security
|
||||
@@ -17,6 +23,7 @@ export default class MobileSecurity {
|
||||
// Panels
|
||||
readonly generalMobileSecurity: GeneralMobileSecurityPanel;
|
||||
readonly microsoftIntune: MicrosoftIntunePanel;
|
||||
readonly mobileEphemeralMode: MobileEphemeralModePanel;
|
||||
|
||||
// Save section
|
||||
readonly saveButton: Locator;
|
||||
@@ -33,6 +40,9 @@ export default class MobileSecurity {
|
||||
this.microsoftIntune = new MicrosoftIntunePanel(
|
||||
container.locator('.AdminSectionPanel').filter({hasText: 'Microsoft Intune'}),
|
||||
);
|
||||
this.mobileEphemeralMode = new MobileEphemeralModePanel(
|
||||
container.locator('.AdminSectionPanel').filter({hasText: 'Mobile Ephemeral Mode'}),
|
||||
);
|
||||
|
||||
this.saveButton = container.getByRole('button', {name: 'Save'});
|
||||
this.errorMessage = container.locator('.error-message');
|
||||
@@ -77,6 +87,20 @@ export default class MobileSecurity {
|
||||
get clientId() {
|
||||
return this.microsoftIntune.clientId;
|
||||
}
|
||||
|
||||
// Convenience shortcuts for Mobile Ephemeral Mode settings
|
||||
get enableMobileEphemeralMode() {
|
||||
return this.mobileEphemeralMode.enableMobileEphemeralMode;
|
||||
}
|
||||
get disconnectionTimeout() {
|
||||
return this.mobileEphemeralMode.disconnectionTimeout;
|
||||
}
|
||||
get offlinePersistenceTimer() {
|
||||
return this.mobileEphemeralMode.offlinePersistenceTimer;
|
||||
}
|
||||
get autoCacheCleanup() {
|
||||
return this.mobileEphemeralMode.autoCacheCleanup;
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralMobileSecurityPanel extends AdminSectionPanel {
|
||||
@@ -105,6 +129,33 @@ class GeneralMobileSecurityPanel extends AdminSectionPanel {
|
||||
}
|
||||
}
|
||||
|
||||
class MobileEphemeralModePanel extends AdminSectionPanel {
|
||||
readonly enableMobileEphemeralMode: RadioSetting;
|
||||
readonly disconnectionTimeout: NumberInputSetting;
|
||||
readonly offlinePersistenceTimer: NumberInputSetting;
|
||||
readonly autoCacheCleanup: NumberInputSetting;
|
||||
|
||||
constructor(container: Locator) {
|
||||
super(container, 'Mobile Ephemeral Mode');
|
||||
|
||||
this.enableMobileEphemeralMode = new RadioSetting(
|
||||
this.body.getByRole('group', {name: /Enable Mobile Ephemeral Mode/}),
|
||||
);
|
||||
this.disconnectionTimeout = new NumberInputSetting(
|
||||
this.body.locator('.form-group').filter({hasText: 'Disconnection Timeout (seconds):'}),
|
||||
'Disconnection Timeout (seconds):',
|
||||
);
|
||||
this.offlinePersistenceTimer = new NumberInputSetting(
|
||||
this.body.locator('.form-group').filter({hasText: 'Offline Persistence Timer (hours):'}),
|
||||
'Offline Persistence Timer (hours):',
|
||||
);
|
||||
this.autoCacheCleanup = new NumberInputSetting(
|
||||
this.body.locator('.form-group').filter({hasText: 'Auto Cache Cleanup (days):'}),
|
||||
'Auto Cache Cleanup (days):',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MicrosoftIntunePanel extends AdminSectionPanel {
|
||||
readonly enableIntuneMAM: RadioSetting;
|
||||
readonly authProvider: DropdownSetting;
|
||||
|
||||
@@ -507,3 +507,231 @@ test('should disable Intune inputs when toggle is off', async ({pw}) => {
|
||||
expect(await systemConsolePage.mobileSecurity.tenantId.input.isDisabled()).toBe(false);
|
||||
expect(await systemConsolePage.mobileSecurity.clientId.input.isDisabled()).toBe(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify timer settings are disabled when Mobile Ephemeral Mode is not enabled, and become editable when enabled
|
||||
*/
|
||||
test(
|
||||
'should disable Mobile Ephemeral Mode sub-settings when toggle is off and enable them when toggle is on',
|
||||
{tag: '@mobile_ephemeral_mode'},
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
const config = await adminClient.getConfig();
|
||||
test.skip(
|
||||
config.FeatureFlags.MobileEphemeralMode !== true && config.FeatureFlags.MobileEphemeralMode !== 'true',
|
||||
'Skipping test - MobileEphemeralMode feature flag is not enabled on the server',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.mobileSecurity.click();
|
||||
await systemConsolePage.mobileSecurity.toBeVisible();
|
||||
|
||||
// * Verify Mobile Ephemeral Mode toggle is off by default
|
||||
await systemConsolePage.mobileSecurity.enableMobileEphemeralMode.toBeFalse();
|
||||
|
||||
// * Verify all sub-settings are disabled
|
||||
expect(await systemConsolePage.mobileSecurity.disconnectionTimeout.input.isDisabled()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.input.isDisabled()).toBe(true);
|
||||
expect(await systemConsolePage.mobileSecurity.autoCacheCleanup.input.isDisabled()).toBe(true);
|
||||
|
||||
// # Enable Mobile Ephemeral Mode toggle
|
||||
await systemConsolePage.mobileSecurity.enableMobileEphemeralMode.selectTrue();
|
||||
|
||||
// * Verify all sub-settings are now enabled
|
||||
expect(await systemConsolePage.mobileSecurity.disconnectionTimeout.input.isDisabled()).toBe(false);
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.input.isDisabled()).toBe(false);
|
||||
expect(await systemConsolePage.mobileSecurity.autoCacheCleanup.input.isDisabled()).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify all Mobile Ephemeral Mode settings persist after save and navigation
|
||||
*/
|
||||
test(
|
||||
'should save and persist all Mobile Ephemeral Mode settings after navigation',
|
||||
{tag: '@mobile_ephemeral_mode'},
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
const config = await adminClient.getConfig();
|
||||
test.skip(
|
||||
config.FeatureFlags.MobileEphemeralMode !== true && config.FeatureFlags.MobileEphemeralMode !== 'true',
|
||||
'Skipping test - MobileEphemeralMode feature flag is not enabled on the server',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Enable Mobile Ephemeral Mode setting via config API
|
||||
config.MobileEphemeralModeSettings.Enable = true;
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.mobileSecurity.click();
|
||||
await systemConsolePage.mobileSecurity.toBeVisible();
|
||||
|
||||
// # Set custom values
|
||||
await systemConsolePage.mobileSecurity.disconnectionTimeout.fill('120');
|
||||
await systemConsolePage.mobileSecurity.offlinePersistenceTimer.fill('48');
|
||||
await systemConsolePage.mobileSecurity.autoCacheCleanup.fill('14');
|
||||
|
||||
// # Save settings
|
||||
await systemConsolePage.mobileSecurity.save();
|
||||
await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
|
||||
|
||||
// # Navigate away and back
|
||||
await systemConsolePage.sidebar.users.click();
|
||||
await systemConsolePage.users.toBeVisible();
|
||||
await systemConsolePage.sidebar.mobileSecurity.click();
|
||||
await systemConsolePage.mobileSecurity.toBeVisible();
|
||||
|
||||
// * Verify Mobile Ephemeral Mode is still enabled
|
||||
await systemConsolePage.mobileSecurity.enableMobileEphemeralMode.toBeTrue();
|
||||
|
||||
// * Verify all values persisted correctly
|
||||
expect(await systemConsolePage.mobileSecurity.disconnectionTimeout.getValue()).toBe('120');
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.getValue()).toBe('48');
|
||||
expect(await systemConsolePage.mobileSecurity.autoCacheCleanup.getValue()).toBe('14');
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify offline persistence timer is disabled when auto cache cleanup is set to 0 (zero-persistence mode)
|
||||
*/
|
||||
test(
|
||||
'should disable offline persistence timer when auto cache cleanup is set to zero',
|
||||
{tag: '@mobile_ephemeral_mode'},
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
const config = await adminClient.getConfig();
|
||||
test.skip(
|
||||
config.FeatureFlags.MobileEphemeralMode !== true && config.FeatureFlags.MobileEphemeralMode !== 'true',
|
||||
'Skipping test - MobileEphemeralMode feature flag is not enabled on the server',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Enable Mobile Ephemeral Mode setting via config API
|
||||
config.MobileEphemeralModeSettings.Enable = true;
|
||||
await adminClient.updateConfig(config);
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.mobileSecurity.click();
|
||||
await systemConsolePage.mobileSecurity.toBeVisible();
|
||||
|
||||
// * Verify offline persistence timer is enabled
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.input.isDisabled()).toBe(false);
|
||||
|
||||
// # Set auto cache cleanup to 0
|
||||
await systemConsolePage.mobileSecurity.autoCacheCleanup.clear();
|
||||
await systemConsolePage.mobileSecurity.autoCacheCleanup.fill('0');
|
||||
|
||||
// * Verify offline persistence timer is now disabled
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.input.isDisabled()).toBe(true);
|
||||
|
||||
// # Set auto cache cleanup back to 7
|
||||
await systemConsolePage.mobileSecurity.autoCacheCleanup.clear();
|
||||
await systemConsolePage.mobileSecurity.autoCacheCleanup.fill('7');
|
||||
|
||||
// * Verify offline persistence timer is enabled again
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.input.isDisabled()).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify Mobile Ephemeral Mode settings show correct defaults on first enable
|
||||
*/
|
||||
test(
|
||||
'should show correct default values when Mobile Ephemeral Mode is first enabled',
|
||||
{tag: '@mobile_ephemeral_mode'},
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
test.skip(
|
||||
license.SkuShortName !== 'advanced',
|
||||
'Skipping test - server does not have enterprise advanced license',
|
||||
);
|
||||
|
||||
const config = await adminClient.getConfig();
|
||||
test.skip(
|
||||
config.FeatureFlags.MobileEphemeralMode !== true && config.FeatureFlags.MobileEphemeralMode !== 'true',
|
||||
'Skipping test - MobileEphemeralMode feature flag is not enabled on the server',
|
||||
);
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to create admin user');
|
||||
}
|
||||
|
||||
// # Log in as admin
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit system console
|
||||
await systemConsolePage.goto();
|
||||
await systemConsolePage.toBeVisible();
|
||||
|
||||
// # Go to Mobile Security section
|
||||
await systemConsolePage.sidebar.mobileSecurity.click();
|
||||
await systemConsolePage.mobileSecurity.toBeVisible();
|
||||
|
||||
// # Enable Mobile Ephemeral Mode
|
||||
await systemConsolePage.mobileSecurity.enableMobileEphemeralMode.selectTrue();
|
||||
|
||||
// * Verify default values
|
||||
expect(await systemConsolePage.mobileSecurity.disconnectionTimeout.getValue()).toBe('60');
|
||||
expect(await systemConsolePage.mobileSecurity.offlinePersistenceTimer.getValue()).toBe('24');
|
||||
expect(await systemConsolePage.mobileSecurity.autoCacheCleanup.getValue()).toBe('7');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -255,6 +255,20 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
||||
props["AutoTranslationLanguages"] = ""
|
||||
}
|
||||
props["RestrictDMAndGMAutotranslation"] = strconv.FormatBool(*c.AutoTranslationSettings.RestrictDMAndGM)
|
||||
|
||||
if c.FeatureFlags.MobileEphemeralMode {
|
||||
ephemeralEnabled := c.MobileEphemeralModeSettings.Enable != nil && *c.MobileEphemeralModeSettings.Enable
|
||||
props["MobileEphemeralModeEnabled"] = strconv.FormatBool(ephemeralEnabled)
|
||||
if c.MobileEphemeralModeSettings.DisconnectionTimeoutSeconds != nil {
|
||||
props["MobileEphemeralModeDisconnectionTimeoutSeconds"] = strconv.Itoa(*c.MobileEphemeralModeSettings.DisconnectionTimeoutSeconds)
|
||||
}
|
||||
if c.MobileEphemeralModeSettings.OfflinePersistenceTimerHours != nil {
|
||||
props["MobileEphemeralModeOfflinePersistenceTimerHours"] = strconv.Itoa(*c.MobileEphemeralModeSettings.OfflinePersistenceTimerHours)
|
||||
}
|
||||
if c.MobileEphemeralModeSettings.AutoCacheCleanupDays != nil {
|
||||
props["MobileEphemeralModeAutoCacheCleanupDays"] = strconv.Itoa(*c.MobileEphemeralModeSettings.AutoCacheCleanupDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
telemetryID string
|
||||
license *model.License
|
||||
expectedFields map[string]string
|
||||
absentFields []string
|
||||
}{
|
||||
{
|
||||
"unlicensed",
|
||||
@@ -48,6 +49,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"WebsocketPort": "80",
|
||||
"WebsocketSecurePort": "443",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"licensed, but not for theme management",
|
||||
@@ -71,6 +73,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EmailNotificationContentsType": "full",
|
||||
"AllowCustomThemes": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"licensed for theme management",
|
||||
@@ -93,6 +96,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EmailNotificationContentsType": "full",
|
||||
"AllowCustomThemes": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"licensed for enforcement",
|
||||
@@ -110,6 +114,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnforceMultifactorAuthentication": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"default marketplace",
|
||||
@@ -123,6 +128,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IsDefaultMarketplace": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"non-default marketplace",
|
||||
@@ -136,6 +142,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IsDefaultMarketplace": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"enable ShowFullName prop",
|
||||
@@ -149,6 +156,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ShowFullName": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"enable UseAnonymousURLs prop",
|
||||
@@ -162,6 +170,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"UseAnonymousURLs": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Custom groups professional license",
|
||||
@@ -174,6 +183,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableCustomGroups": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Custom groups enterprise license",
|
||||
@@ -186,6 +196,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableCustomGroups": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Custom groups other license",
|
||||
@@ -198,6 +209,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableCustomGroups": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Shared channels other license",
|
||||
@@ -216,6 +228,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ExperimentalSharedChannels": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"licensed for shared channels",
|
||||
@@ -234,6 +247,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ExperimentalSharedChannels": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Shared channels professional license",
|
||||
@@ -252,6 +266,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ExperimentalSharedChannels": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"disable EnableUserStatuses",
|
||||
@@ -265,6 +280,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableUserStatuses": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Shared channels enterprise license",
|
||||
@@ -283,6 +299,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ExperimentalSharedChannels": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Disable App Bar",
|
||||
@@ -296,6 +313,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"DisableAppBar": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"default EnableJoinLeaveMessage",
|
||||
@@ -305,6 +323,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableJoinLeaveMessageByDefault": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"disable EnableJoinLeaveMessage",
|
||||
@@ -318,6 +337,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"EnableJoinLeaveMessageByDefault": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"test key for GiphySdkKey",
|
||||
@@ -331,6 +351,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"GiphySdkKey": model.ServiceSettingsDefaultGiphySdkKeyTest,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"report a problem values",
|
||||
@@ -350,6 +371,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"ReportAProblemMail": "mail",
|
||||
"AllowDownloadLogs": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"access control settings enabled",
|
||||
@@ -365,6 +387,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableAttributeBasedAccessControl": "true",
|
||||
"EnableUserManagedAttributes": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"access control settings disabled",
|
||||
@@ -380,6 +403,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableAttributeBasedAccessControl": "false",
|
||||
"EnableUserManagedAttributes": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"access control settings default",
|
||||
@@ -390,6 +414,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableAttributeBasedAccessControl": "false",
|
||||
"EnableUserManagedAttributes": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"burn on read enabled",
|
||||
@@ -405,6 +430,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableBurnOnRead": "true",
|
||||
"BurnOnReadDurationSeconds": "1800",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"burn on read disabled",
|
||||
@@ -420,6 +446,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableBurnOnRead": "false",
|
||||
"BurnOnReadDurationSeconds": "600",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"burn on read default",
|
||||
@@ -430,6 +457,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"EnableBurnOnRead": "true",
|
||||
"BurnOnReadDurationSeconds": "600", // 10 minutes in seconds
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"mobile watermark uses experimental settings",
|
||||
@@ -446,6 +474,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"ExperimentalEnableWatermark": "true",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM enabled with Enterprise Advanced license and Office365 AuthService",
|
||||
@@ -466,6 +495,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"IntuneMAMEnabled": "true",
|
||||
"IntuneScope": "api://87654321-4321-4321-4321-210987654321/login.mattermost",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when not enabled",
|
||||
@@ -485,6 +515,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when TenantId is missing",
|
||||
@@ -504,6 +535,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when ClientId is missing",
|
||||
@@ -523,6 +555,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM not exposed with lower license tier",
|
||||
@@ -540,6 +573,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
SkuShortName: model.LicenseShortSkuProfessional,
|
||||
},
|
||||
map[string]string{},
|
||||
[]string{"IntuneMAMEnabled", "IntuneScope"},
|
||||
},
|
||||
{
|
||||
"Intune MAM not exposed without license",
|
||||
@@ -554,6 +588,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"",
|
||||
nil,
|
||||
map[string]string{},
|
||||
[]string{"IntuneMAMEnabled", "IntuneScope"},
|
||||
},
|
||||
{
|
||||
"Intune MAM enabled with Enterprise Advanced license and SAML AuthService",
|
||||
@@ -578,6 +613,7 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"IntuneScope": "api://87654321-4321-4321-4321-210987654321/login.mattermost",
|
||||
"IntuneAuthService": "saml",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Intune MAM disabled when AuthService is missing",
|
||||
@@ -597,6 +633,100 @@ func TestGetClientConfig(t *testing.T) {
|
||||
map[string]string{
|
||||
"IntuneMAMEnabled": "false",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Mobile Ephemeral Mode enabled with custom values",
|
||||
&model.Config{
|
||||
FeatureFlags: &model.FeatureFlags{MobileEphemeralMode: true},
|
||||
MobileEphemeralModeSettings: model.MobileEphemeralModeSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: model.NewPointer(120),
|
||||
OfflinePersistenceTimerHours: model.NewPointer(48),
|
||||
AutoCacheCleanupDays: model.NewPointer(14),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"MobileEphemeralModeEnabled": "true",
|
||||
"MobileEphemeralModeDisconnectionTimeoutSeconds": "120",
|
||||
"MobileEphemeralModeOfflinePersistenceTimerHours": "48",
|
||||
"MobileEphemeralModeAutoCacheCleanupDays": "14",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Mobile Ephemeral Mode disabled still exposes parameters",
|
||||
&model.Config{
|
||||
FeatureFlags: &model.FeatureFlags{MobileEphemeralMode: true},
|
||||
MobileEphemeralModeSettings: model.MobileEphemeralModeSettings{
|
||||
Enable: model.NewPointer(false),
|
||||
DisconnectionTimeoutSeconds: model.NewPointer(60),
|
||||
OfflinePersistenceTimerHours: model.NewPointer(24),
|
||||
AutoCacheCleanupDays: model.NewPointer(7),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{
|
||||
"MobileEphemeralModeEnabled": "false",
|
||||
"MobileEphemeralModeDisconnectionTimeoutSeconds": "60",
|
||||
"MobileEphemeralModeOfflinePersistenceTimerHours": "24",
|
||||
"MobileEphemeralModeAutoCacheCleanupDays": "7",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"Mobile Ephemeral Mode not exposed when feature flag is off",
|
||||
&model.Config{
|
||||
FeatureFlags: &model.FeatureFlags{MobileEphemeralMode: false},
|
||||
MobileEphemeralModeSettings: model.MobileEphemeralModeSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuEnterpriseAdvanced,
|
||||
},
|
||||
map[string]string{},
|
||||
[]string{"MobileEphemeralModeEnabled", "MobileEphemeralModeDisconnectionTimeoutSeconds", "MobileEphemeralModeOfflinePersistenceTimerHours", "MobileEphemeralModeAutoCacheCleanupDays"},
|
||||
},
|
||||
{
|
||||
"Mobile Ephemeral Mode not exposed without license",
|
||||
&model.Config{
|
||||
FeatureFlags: &model.FeatureFlags{MobileEphemeralMode: true},
|
||||
MobileEphemeralModeSettings: model.MobileEphemeralModeSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
},
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
map[string]string{},
|
||||
[]string{"MobileEphemeralModeEnabled", "MobileEphemeralModeDisconnectionTimeoutSeconds", "MobileEphemeralModeOfflinePersistenceTimerHours", "MobileEphemeralModeAutoCacheCleanupDays"},
|
||||
},
|
||||
{
|
||||
"Mobile Ephemeral Mode not exposed with lower license tier",
|
||||
&model.Config{
|
||||
FeatureFlags: &model.FeatureFlags{MobileEphemeralMode: true},
|
||||
MobileEphemeralModeSettings: model.MobileEphemeralModeSettings{
|
||||
Enable: model.NewPointer(true),
|
||||
},
|
||||
},
|
||||
"",
|
||||
&model.License{
|
||||
Features: &model.Features{},
|
||||
SkuShortName: model.LicenseShortSkuProfessional,
|
||||
},
|
||||
map[string]string{},
|
||||
[]string{"MobileEphemeralModeEnabled", "MobileEphemeralModeDisconnectionTimeoutSeconds", "MobileEphemeralModeOfflinePersistenceTimerHours", "MobileEphemeralModeAutoCacheCleanupDays"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -616,6 +746,10 @@ func TestGetClientConfig(t *testing.T) {
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
for _, absentField := range testCase.absentFields {
|
||||
_, ok := configMap[absentField]
|
||||
assert.False(t, ok, fmt.Sprintf("config should not contain %v", absentField))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11542,6 +11542,18 @@
|
||||
"id": "model.config.is_valid.minimum_desktop_app_version.app_error",
|
||||
"translation": "Invalid version number. Must be a valid semantic version (e.g. 5.0.0)."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
|
||||
"translation": "Invalid Auto Cache Cleanup value. Must be between {{.Min}} and {{.Max}} days."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
|
||||
"translation": "Invalid Disconnection Timeout value. Must be between {{.Min}} and {{.Max}} seconds."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
|
||||
"translation": "Invalid Offline Persistence Timer value. Must be between {{.Min}} and {{.Max}} hours."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.move_thread.domain_invalid.app_error",
|
||||
"translation": "Invalid domain for move thread settings"
|
||||
|
||||
@@ -3442,6 +3442,61 @@ func (s *DataRetentionSettings) GetFileRetentionHours() int {
|
||||
return DataRetentionSettingsDefaultFileRetentionDays * 24
|
||||
}
|
||||
|
||||
const (
|
||||
MobileEphemeralModeDefaultDisconnectionTimeoutSeconds = 60
|
||||
MobileEphemeralModeDefaultOfflinePersistenceTimerHours = 24
|
||||
MobileEphemeralModeDefaultAutoCacheCleanupDays = 7
|
||||
|
||||
MobileEphemeralModeMaxDisconnectionTimeoutSeconds = 600
|
||||
MobileEphemeralModeMaxOfflinePersistenceTimerHours = 72
|
||||
MobileEphemeralModeMaxAutoCacheCleanupDays = 60
|
||||
)
|
||||
|
||||
type MobileEphemeralModeSettings struct {
|
||||
Enable *bool `access:"environment_mobile_security"`
|
||||
DisconnectionTimeoutSeconds *int `access:"environment_mobile_security"`
|
||||
OfflinePersistenceTimerHours *int `access:"environment_mobile_security"`
|
||||
AutoCacheCleanupDays *int `access:"environment_mobile_security"`
|
||||
}
|
||||
|
||||
func (s *MobileEphemeralModeSettings) SetDefaults() {
|
||||
if s.Enable == nil {
|
||||
s.Enable = NewPointer(false)
|
||||
}
|
||||
if s.DisconnectionTimeoutSeconds == nil {
|
||||
s.DisconnectionTimeoutSeconds = NewPointer(MobileEphemeralModeDefaultDisconnectionTimeoutSeconds)
|
||||
}
|
||||
if s.OfflinePersistenceTimerHours == nil {
|
||||
s.OfflinePersistenceTimerHours = NewPointer(MobileEphemeralModeDefaultOfflinePersistenceTimerHours)
|
||||
}
|
||||
if s.AutoCacheCleanupDays == nil {
|
||||
s.AutoCacheCleanupDays = NewPointer(MobileEphemeralModeDefaultAutoCacheCleanupDays)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MobileEphemeralModeSettings) isValid() *AppError {
|
||||
if s.Enable == nil || !*s.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.DisconnectionTimeoutSeconds == nil || *s.DisconnectionTimeoutSeconds < 0 || *s.DisconnectionTimeoutSeconds > MobileEphemeralModeMaxDisconnectionTimeoutSeconds {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
|
||||
map[string]any{"Min": 0, "Max": MobileEphemeralModeMaxDisconnectionTimeoutSeconds}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if s.OfflinePersistenceTimerHours == nil || *s.OfflinePersistenceTimerHours < 0 || *s.OfflinePersistenceTimerHours > MobileEphemeralModeMaxOfflinePersistenceTimerHours {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
|
||||
map[string]any{"Min": 0, "Max": MobileEphemeralModeMaxOfflinePersistenceTimerHours}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if s.AutoCacheCleanupDays == nil || *s.AutoCacheCleanupDays < 0 || *s.AutoCacheCleanupDays > MobileEphemeralModeMaxAutoCacheCleanupDays {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
|
||||
map[string]any{"Min": 0, "Max": MobileEphemeralModeMaxAutoCacheCleanupDays}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type JobSettings struct {
|
||||
RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
||||
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
||||
@@ -4079,6 +4134,7 @@ type Config struct {
|
||||
AnalyticsSettings AnalyticsSettings
|
||||
ElasticsearchSettings ElasticsearchSettings
|
||||
DataRetentionSettings DataRetentionSettings
|
||||
MobileEphemeralModeSettings MobileEphemeralModeSettings
|
||||
MessageExportSettings MessageExportSettings
|
||||
JobSettings JobSettings
|
||||
PluginSettings PluginSettings
|
||||
@@ -4194,6 +4250,7 @@ func (o *Config) SetDefaults() {
|
||||
o.NativeAppSettings.SetDefaults()
|
||||
o.IntuneSettings.SetDefaults()
|
||||
o.DataRetentionSettings.SetDefaults()
|
||||
o.MobileEphemeralModeSettings.SetDefaults()
|
||||
o.RateLimitSettings.SetDefaults()
|
||||
o.LogSettings.SetDefaults()
|
||||
o.ExperimentalAuditSettings.SetDefaults()
|
||||
@@ -4373,6 +4430,10 @@ func (o *Config) IsValid() *AppError {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if appErr := o.MobileEphemeralModeSettings.isValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if appErr := o.GuestAccountsSettings.IsValid(); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
@@ -2975,6 +2975,121 @@ func TestConfigAccessTagsMapToValidPermissions(t *testing.T) {
|
||||
checkStruct(t, reflect.TypeFor[Config](), "Config")
|
||||
}
|
||||
|
||||
func TestMobileEphemeralModeSettingsDefaults(t *testing.T) {
|
||||
c := Config{}
|
||||
c.SetDefaults()
|
||||
|
||||
require.False(t, *c.MobileEphemeralModeSettings.Enable)
|
||||
require.Equal(t, MobileEphemeralModeDefaultDisconnectionTimeoutSeconds, *c.MobileEphemeralModeSettings.DisconnectionTimeoutSeconds)
|
||||
require.Equal(t, MobileEphemeralModeDefaultOfflinePersistenceTimerHours, *c.MobileEphemeralModeSettings.OfflinePersistenceTimerHours)
|
||||
require.Equal(t, MobileEphemeralModeDefaultAutoCacheCleanupDays, *c.MobileEphemeralModeSettings.AutoCacheCleanupDays)
|
||||
}
|
||||
|
||||
func TestMobileEphemeralModeSettingsIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
settings MobileEphemeralModeSettings
|
||||
expectError bool
|
||||
errorId string
|
||||
}{
|
||||
{
|
||||
name: "disabled settings should be valid",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(false),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "enabled with valid values",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(120),
|
||||
OfflinePersistenceTimerHours: NewPointer(24),
|
||||
AutoCacheCleanupDays: NewPointer(7),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid disconnection timeout above max",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(MobileEphemeralModeMaxDisconnectionTimeoutSeconds + 1),
|
||||
OfflinePersistenceTimerHours: NewPointer(0),
|
||||
AutoCacheCleanupDays: NewPointer(0),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
|
||||
},
|
||||
{
|
||||
name: "invalid offline persistence above max",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(60),
|
||||
OfflinePersistenceTimerHours: NewPointer(MobileEphemeralModeMaxOfflinePersistenceTimerHours + 1),
|
||||
AutoCacheCleanupDays: NewPointer(0),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
|
||||
},
|
||||
{
|
||||
name: "invalid auto cache cleanup above max",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(60),
|
||||
OfflinePersistenceTimerHours: NewPointer(0),
|
||||
AutoCacheCleanupDays: NewPointer(MobileEphemeralModeMaxAutoCacheCleanupDays + 1),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
|
||||
},
|
||||
{
|
||||
name: "invalid negative disconnection timeout",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(-1),
|
||||
OfflinePersistenceTimerHours: NewPointer(0),
|
||||
AutoCacheCleanupDays: NewPointer(0),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
|
||||
},
|
||||
{
|
||||
name: "invalid negative offline persistence",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(60),
|
||||
OfflinePersistenceTimerHours: NewPointer(-1),
|
||||
AutoCacheCleanupDays: NewPointer(0),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
|
||||
},
|
||||
{
|
||||
name: "invalid negative auto cache cleanup",
|
||||
settings: MobileEphemeralModeSettings{
|
||||
Enable: NewPointer(true),
|
||||
DisconnectionTimeoutSeconds: NewPointer(60),
|
||||
OfflinePersistenceTimerHours: NewPointer(0),
|
||||
AutoCacheCleanupDays: NewPointer(-1),
|
||||
},
|
||||
expectError: true,
|
||||
errorId: "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.settings.isValid()
|
||||
if tc.expectError {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, tc.errorId, err.Id)
|
||||
} else {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeAppSettingsIsValid(t *testing.T) {
|
||||
t.Run("defaults are valid", func(t *testing.T) {
|
||||
cfg := Config{}
|
||||
|
||||
@@ -125,6 +125,9 @@ type FeatureFlags struct {
|
||||
// Gates the per-channel Discoverable toggle and the channel-join-request flow that lets
|
||||
// non-members find a private channel in Browse Channels and request to join it.
|
||||
DiscoverableChannels bool
|
||||
|
||||
// Enable Mobile Ephemeral Mode for controlling data persistence on mobile devices
|
||||
MobileEphemeralMode bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -183,6 +186,8 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.ManagedChannelCategories = false
|
||||
|
||||
f.DiscoverableChannels = false
|
||||
|
||||
f.MobileEphemeralMode = false
|
||||
}
|
||||
|
||||
// ToMap returns the feature flags as a map[string]string
|
||||
|
||||
@@ -2373,6 +2373,76 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'MobileSecuritySettings.EphemeralMode',
|
||||
title: 'Mobile Ephemeral Mode',
|
||||
description: defineMessage({id: 'admin.mobileSecurity.sections.ephemeralMode.description', defaultMessage: 'Configure data persistence and cache management policies for mobile devices.'}),
|
||||
license_sku: LicenseSkus.EnterpriseAdvanced,
|
||||
component: LicensedSectionContainer,
|
||||
componentProps: {
|
||||
requiredSku: LicenseSkus.EnterpriseAdvanced,
|
||||
featureDiscoveryConfig: {
|
||||
featureName: 'mobile_ephemeral_mode',
|
||||
title: defineMessage({id: 'admin.mobileSecurity.ephemeralMode_feature_discovery.title', defaultMessage: 'Control mobile data persistence with Mobile Ephemeral Mode'}),
|
||||
description: defineMessage({id: 'admin.mobileSecurity.ephemeralMode_feature_discovery.description', defaultMessage: 'With Mattermost Enterprise Advanced, you can enable Mobile Ephemeral Mode to enforce data persistence policies on mobile devices. Configure disconnection timeouts, offline data retention, and automatic cache cleanup.'}),
|
||||
learnMoreURL: 'https://docs.mattermost.com',
|
||||
},
|
||||
},
|
||||
isHidden: it.configIsFalse('FeatureFlags', 'MobileEphemeralMode'),
|
||||
settings: [
|
||||
{
|
||||
type: 'banner',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.banner', defaultMessage: 'Changes to these settings are delivered to connected devices in real time. Offline devices will continue operating under their last-known settings until they re-establish a server connection. Timer state persists across app and device restarts.'}),
|
||||
banner_type: 'info',
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'MobileEphemeralModeSettings.Enable',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.enableTitle', defaultMessage: 'Enable Mobile Ephemeral Mode:'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.enableDescription', defaultMessage: 'When enabled, mobile clients will follow the server-configured ephemeral data policies. Disconnected devices will clean up cached data based on the configured timers.'}),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'MobileEphemeralModeSettings.DisconnectionTimeoutSeconds',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.disconnectionTimeoutTitle', defaultMessage: 'Disconnection Timeout (seconds):'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.disconnectionTimeoutDescription', defaultMessage: 'Grace period after losing server connection before the device is considered offline. Helps avoid false triggers from brief network interruptions. Values below 5 are not recommended.'}),
|
||||
placeholder: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.disconnectionTimeout.placeholder', defaultMessage: 'E.g.: 60'}),
|
||||
isDisabled: it.stateIsFalse('MobileEphemeralModeSettings.Enable'),
|
||||
validate: validators.numberInRange(0, 600, defineMessage({
|
||||
id: 'admin.mobileSecurity.ephemeralMode.disconnectionTimeout.range',
|
||||
defaultMessage: 'Must be a number between 0 and 600 seconds (10 minutes).',
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'MobileEphemeralModeSettings.OfflinePersistenceTimerHours',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.offlinePersistenceTitle', defaultMessage: 'Offline Persistence Timer (hours):'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.offlinePersistenceDescription', defaultMessage: 'How long cached content is kept after the device goes offline. When the timer expires, cached content is deleted but session credentials are preserved. Set to 0 for immediate cleanup.'}),
|
||||
disabled_help_text: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.offlinePersistence.disabled', defaultMessage: 'How long cached content is kept after the device goes offline. When the timer expires, cached content is deleted but session credentials are preserved. Set to 0 for immediate cleanup. Requires Mobile Ephemeral Mode to be enabled and Auto Cache Cleanup to be greater than 0.'}),
|
||||
placeholder: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.offlinePersistence.placeholder', defaultMessage: 'E.g.: 24'}),
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('MobileEphemeralModeSettings.Enable'),
|
||||
it.stateEquals('MobileEphemeralModeSettings.AutoCacheCleanupDays', 0),
|
||||
),
|
||||
validate: validators.numberInRange(0, 72, defineMessage({
|
||||
id: 'admin.mobileSecurity.ephemeralMode.offlinePersistence.range',
|
||||
defaultMessage: 'Must be a number between 0 and 72 hours (3 days).',
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'MobileEphemeralModeSettings.AutoCacheCleanupDays',
|
||||
label: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.autoCacheCleanupTitle', defaultMessage: 'Auto Cache Cleanup (days):'}),
|
||||
help_text: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.autoCacheCleanupDescription', defaultMessage: 'Controls the maximum age of any content cached on the device, regardless of connection status. Prevents unbounded accumulation of sensitive data. Set to 0 for zero-persistence mode where content is never persisted to disk.'}),
|
||||
placeholder: defineMessage({id: 'admin.mobileSecurity.ephemeralMode.autoCacheCleanup.placeholder', defaultMessage: 'E.g.: 7'}),
|
||||
isDisabled: it.stateIsFalse('MobileEphemeralModeSettings.Enable'),
|
||||
validate: validators.numberInRange(0, 60, defineMessage({
|
||||
id: 'admin.mobileSecurity.ephemeralMode.autoCacheCleanup.range',
|
||||
defaultMessage: 'Must be a number between 0 and 60 days.',
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {it} from './admin_definition_helpers';
|
||||
import {it, validators} from './admin_definition_helpers';
|
||||
|
||||
describe('AdminDefinitionHelpers - stateEqualsOrDefault', () => {
|
||||
test('should return true when state value equals expected value', () => {
|
||||
@@ -45,3 +45,22 @@ describe('AdminDefinitionHelpers - stateEqualsOrDefault', () => {
|
||||
expect(checker({}, undefinedStateWithDifferentExpected)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AdminDefinitionHelpers - validators.numberInRange', () => {
|
||||
const validate = validators.numberInRange(0, 60, 'out of range');
|
||||
|
||||
test('should return valid for in-range numbers', () => {
|
||||
expect(validate(0).isValid()).toBe(true);
|
||||
expect(validate(30).isValid()).toBe(true);
|
||||
expect(validate(60).isValid()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return invalid for out-of-range numbers', () => {
|
||||
expect(validate(-1).isValid()).toBe(false);
|
||||
expect(validate(61).isValid()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return valid for NaN since the server backfills empty inputs with defaults', () => {
|
||||
expect(validate(NaN).isValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ export const validators = {
|
||||
isRequired: (text: MessageDescriptor | string) => (value: string) => new ValidationResult(Boolean(value), text),
|
||||
minValue: (min: number, text: MessageDescriptor | string) => (value: number) => new ValidationResult((value >= min), text),
|
||||
maxValue: (max: number, text: MessageDescriptor | string) => (value: number) => new ValidationResult((value <= max), text),
|
||||
numberInRange: (min: number, max: number, text: MessageDescriptor | string) => (value: number) => new ValidationResult(Number.isNaN(value) || (value >= min && value <= max), text),
|
||||
};
|
||||
|
||||
export const usesLegacyOauth = (config: Partial<AdminConfig>, state: any, license?: ClientLicense, enterpriseReady?: boolean, consoleAccess?: ConsoleAccess, cloud?: CloudState) => {
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {AdminConfig} from '@mattermost/types/config';
|
||||
|
||||
import {LicenseSkus} from 'utils/constants';
|
||||
|
||||
import AdminDefinition from './admin_definition';
|
||||
import type {AdminDefinitionSetting, AdminDefinitionConfigSchemaSection} from './types';
|
||||
|
||||
describe('AdminDefinition - Mobile Ephemeral Mode Settings', () => {
|
||||
const getEphemeralModeSections = () => {
|
||||
const mobileSecuritySection = AdminDefinition.environment.subsections.mobile_security;
|
||||
const sections = 'sections' in mobileSecuritySection.schema! ? mobileSecuritySection.schema.sections : undefined;
|
||||
return sections;
|
||||
};
|
||||
|
||||
const getEphemeralModeSection = () => {
|
||||
const sections = getEphemeralModeSections();
|
||||
return sections?.find((section: AdminDefinitionConfigSchemaSection) => section.key === 'MobileSecuritySettings.EphemeralMode');
|
||||
};
|
||||
|
||||
const getEphemeralModeSettings = () => {
|
||||
const section = getEphemeralModeSection();
|
||||
return section?.settings || [];
|
||||
};
|
||||
|
||||
test('should include Mobile Ephemeral Mode section in mobile_security', () => {
|
||||
const section = getEphemeralModeSection();
|
||||
expect(section).toBeDefined();
|
||||
});
|
||||
|
||||
test('should include Enable setting', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const enableSetting = settings.find((s: AdminDefinitionSetting) => s.key === 'MobileEphemeralModeSettings.Enable');
|
||||
|
||||
expect(enableSetting).toBeDefined();
|
||||
expect(enableSetting?.type).toBe('bool');
|
||||
expect(enableSetting?.label).toBeDefined();
|
||||
expect(enableSetting?.help_text).toBeDefined();
|
||||
});
|
||||
|
||||
test('should include info banner', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const bannerSetting = settings.find((s: AdminDefinitionSetting) => s.type === 'banner');
|
||||
|
||||
expect(bannerSetting).toBeDefined();
|
||||
});
|
||||
|
||||
test('settings should have proper translation message descriptors', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const settingsWithLabels = settings.filter((s: AdminDefinitionSetting) => s.key?.includes('MobileEphemeralMode'));
|
||||
|
||||
settingsWithLabels.forEach((setting: AdminDefinitionSetting) => {
|
||||
if (setting.label && typeof setting.label === 'object') {
|
||||
expect('id' in setting.label).toBe(true);
|
||||
expect('defaultMessage' in setting.label).toBe(true);
|
||||
}
|
||||
|
||||
if (setting.help_text && typeof setting.help_text === 'object' && !('$$typeof' in setting.help_text)) {
|
||||
expect('id' in setting.help_text).toBe(true);
|
||||
expect('defaultMessage' in setting.help_text).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should use LicensedSectionContainer with Enterprise Advanced', () => {
|
||||
const section = getEphemeralModeSection();
|
||||
|
||||
expect(section?.component).toBeDefined();
|
||||
expect(section?.license_sku).toBe(LicenseSkus.EnterpriseAdvanced);
|
||||
expect(section?.componentProps).toBeDefined();
|
||||
expect(section?.componentProps?.requiredSku).toBe(LicenseSkus.EnterpriseAdvanced);
|
||||
expect(section?.componentProps?.featureDiscoveryConfig).toBeDefined();
|
||||
expect(section?.componentProps?.featureDiscoveryConfig?.featureName).toBe('mobile_ephemeral_mode');
|
||||
});
|
||||
|
||||
test('isHidden should return true when feature flag is disabled', () => {
|
||||
const section = getEphemeralModeSection();
|
||||
expect(section?.isHidden).toBeDefined();
|
||||
expect(typeof section?.isHidden).toBe('function');
|
||||
|
||||
const mockConfig: Partial<AdminConfig> = {FeatureFlags: {MobileEphemeralMode: false}};
|
||||
const isHiddenFn = section!.isHidden as (config: Partial<AdminConfig>) => boolean;
|
||||
expect(isHiddenFn(mockConfig)).toBe(true);
|
||||
});
|
||||
|
||||
test('isHidden should return false when feature flag is enabled', () => {
|
||||
const section = getEphemeralModeSection();
|
||||
|
||||
const mockConfig: Partial<AdminConfig> = {FeatureFlags: {MobileEphemeralMode: true}};
|
||||
const isHiddenFn = section!.isHidden as (config: Partial<AdminConfig>) => boolean;
|
||||
expect(isHiddenFn(mockConfig)).toBe(false);
|
||||
});
|
||||
|
||||
test('should include DisconnectionTimeoutSeconds number setting', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const setting = settings.find((s: AdminDefinitionSetting) => s.key === 'MobileEphemeralModeSettings.DisconnectionTimeoutSeconds');
|
||||
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.type).toBe('number');
|
||||
expect(setting?.isDisabled).toBeDefined();
|
||||
});
|
||||
|
||||
test('should include OfflinePersistenceTimerHours number setting', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const setting = settings.find((s: AdminDefinitionSetting) => s.key === 'MobileEphemeralModeSettings.OfflinePersistenceTimerHours');
|
||||
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.type).toBe('number');
|
||||
expect(setting?.isDisabled).toBeDefined();
|
||||
});
|
||||
|
||||
test('should include AutoCacheCleanupDays number setting', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const setting = settings.find((s: AdminDefinitionSetting) => s.key === 'MobileEphemeralModeSettings.AutoCacheCleanupDays');
|
||||
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting?.type).toBe('number');
|
||||
expect(setting?.isDisabled).toBeDefined();
|
||||
});
|
||||
|
||||
test('OfflinePersistenceTimerHours should have disabled_help_text for zero-persistence mode', () => {
|
||||
const settings = getEphemeralModeSettings();
|
||||
const setting = settings.find((s: AdminDefinitionSetting) => s.key === 'MobileEphemeralModeSettings.OfflinePersistenceTimerHours');
|
||||
|
||||
expect(setting?.disabled_help_text).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1870,11 +1870,30 @@
|
||||
"admin.mobileSecurity.allowPdfLinkNavigationTitle": "Allow Link Navigation in Secure PDFs:",
|
||||
"admin.mobileSecurity.biometricsDescription": "Enforces biometric authentication (with PIN/passcode fallback) before accessing the app. Users will be prompted based on session activity and server switching rules.",
|
||||
"admin.mobileSecurity.biometricsTitle": "Enable Biometric Authentication:",
|
||||
"admin.mobileSecurity.ephemeralMode_feature_discovery.description": "With Mattermost Enterprise Advanced, you can enable Mobile Ephemeral Mode to enforce data persistence policies on mobile devices. Configure disconnection timeouts, offline data retention, and automatic cache cleanup.",
|
||||
"admin.mobileSecurity.ephemeralMode_feature_discovery.title": "Control mobile data persistence with Mobile Ephemeral Mode",
|
||||
"admin.mobileSecurity.ephemeralMode.autoCacheCleanup.placeholder": "E.g.: 7",
|
||||
"admin.mobileSecurity.ephemeralMode.autoCacheCleanup.range": "Must be a number between 0 and 60 days.",
|
||||
"admin.mobileSecurity.ephemeralMode.autoCacheCleanupDescription": "Controls the maximum age of any content cached on the device, regardless of connection status. Prevents unbounded accumulation of sensitive data. Set to 0 for zero-persistence mode where content is never persisted to disk.",
|
||||
"admin.mobileSecurity.ephemeralMode.autoCacheCleanupTitle": "Auto Cache Cleanup (days):",
|
||||
"admin.mobileSecurity.ephemeralMode.banner": "Changes to these settings are delivered to connected devices in real time. Offline devices will continue operating under their last-known settings until they re-establish a server connection. Timer state persists across app and device restarts.",
|
||||
"admin.mobileSecurity.ephemeralMode.disconnectionTimeout.placeholder": "E.g.: 60",
|
||||
"admin.mobileSecurity.ephemeralMode.disconnectionTimeout.range": "Must be a number between 0 and 600 seconds (10 minutes).",
|
||||
"admin.mobileSecurity.ephemeralMode.disconnectionTimeoutDescription": "Grace period after losing server connection before the device is considered offline. Helps avoid false triggers from brief network interruptions. Values below 5 are not recommended.",
|
||||
"admin.mobileSecurity.ephemeralMode.disconnectionTimeoutTitle": "Disconnection Timeout (seconds):",
|
||||
"admin.mobileSecurity.ephemeralMode.enableDescription": "When enabled, mobile clients will follow the server-configured ephemeral data policies. Disconnected devices will clean up cached data based on the configured timers.",
|
||||
"admin.mobileSecurity.ephemeralMode.enableTitle": "Enable Mobile Ephemeral Mode:",
|
||||
"admin.mobileSecurity.ephemeralMode.offlinePersistence.disabled": "How long cached content is kept after the device goes offline. When the timer expires, cached content is deleted but session credentials are preserved. Set to 0 for immediate cleanup. Requires Mobile Ephemeral Mode to be enabled and Auto Cache Cleanup to be greater than 0.",
|
||||
"admin.mobileSecurity.ephemeralMode.offlinePersistence.placeholder": "E.g.: 24",
|
||||
"admin.mobileSecurity.ephemeralMode.offlinePersistence.range": "Must be a number between 0 and 72 hours (3 days).",
|
||||
"admin.mobileSecurity.ephemeralMode.offlinePersistenceDescription": "How long cached content is kept after the device goes offline. When the timer expires, cached content is deleted but session credentials are preserved. Set to 0 for immediate cleanup.",
|
||||
"admin.mobileSecurity.ephemeralMode.offlinePersistenceTitle": "Offline Persistence Timer (hours):",
|
||||
"admin.mobileSecurity.jailbreakDescription": "Prevents access to the app on devices detected as jailbroken or rooted. If a device fails the security check, users will be denied access or prompted to switch to a compliant server.",
|
||||
"admin.mobileSecurity.jailbreakTitle": "Enable Jailbreak/Root Protection:",
|
||||
"admin.mobileSecurity.mobileAllowDownloads": "Site Configuration > File Sharing and Downloads > Allow File Downloads on Mobile",
|
||||
"admin.mobileSecurity.screenCaptureDescription": "Blocks screenshots and screen recordings when using the mobile app. Screenshots will appear blank, and screen recordings will blur (iOS) or show a black screen (Android). Also applies when switching apps.",
|
||||
"admin.mobileSecurity.screenCaptureTitle": "Prevent Screen Capture:",
|
||||
"admin.mobileSecurity.sections.ephemeralMode.description": "Configure data persistence and cache management policies for mobile devices.",
|
||||
"admin.mobileSecurity.sections.general.description": "Configure device security features for the mobile app.",
|
||||
"admin.mobileSecurity.sections.intune.description": "Configure Microsoft Intune Mobile Application Management (MAM) for App Protection Policies.",
|
||||
"admin.mobileSecurity.secureFilePreviewDescription": "Prevents file downloads, previews, and sharing for most file types, even if {mobileAllowDownloads} is enabled. Allows in-app previews for PDFs, videos, and images only. Files are stored temporarily in the app's cache and cannot be exported or shared.",
|
||||
|
||||
@@ -29,8 +29,8 @@ describe('AdminConsoleIndex.generateIndex', () => {
|
||||
expect(idx.search('saml')).toEqual([
|
||||
'authentication/saml',
|
||||
'environment/session_lengths',
|
||||
'authentication/email',
|
||||
'environment/mobile_security',
|
||||
'authentication/email',
|
||||
'experimental/features',
|
||||
]);
|
||||
expect(idx.search('nginx')).toEqual([
|
||||
|
||||
@@ -837,6 +837,13 @@ export type IntuneSettings = {
|
||||
AuthService?: string;
|
||||
};
|
||||
|
||||
export type MobileEphemeralModeSettings = {
|
||||
Enable: boolean;
|
||||
DisconnectionTimeoutSeconds: number;
|
||||
OfflinePersistenceTimerHours: number;
|
||||
AutoCacheCleanupDays: number;
|
||||
};
|
||||
|
||||
export type ClusterSettings = {
|
||||
Enable: boolean;
|
||||
ClusterName: string;
|
||||
@@ -1105,6 +1112,7 @@ export type AdminConfig = {
|
||||
AccessControlSettings: AccessControlSettings;
|
||||
ContentFlaggingSettings: ContentFlaggingSettings;
|
||||
AutoTranslationSettings: AutoTranslationSettings;
|
||||
MobileEphemeralModeSettings: MobileEphemeralModeSettings;
|
||||
};
|
||||
|
||||
export type ReplicaLagSetting = {
|
||||
|
||||
Reference in New Issue
Block a user