diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts
index 41674f5d570..e601d58d759 100644
--- a/e2e-tests/playwright/lib/src/server/default_config.ts
+++ b/e2e-tests/playwright/lib/src/server/default_config.ts
@@ -566,6 +566,8 @@ const defaultServerConfig: AdminConfig = {
MobileEnableBiometrics: false,
MobilePreventScreenCapture: false,
MobileJailbreakProtection: false,
+ MobileEnableSecureFilePreview: false,
+ MobileAllowPdfLinkNavigation: false,
},
CacheSettings: {
CacheType: 'lru',
diff --git a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts
index 8bb78d2df29..e5017348c19 100644
--- a/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts
+++ b/e2e-tests/playwright/lib/src/ui/components/system_console/sections/system_users/mobile_security.ts
@@ -15,6 +15,10 @@ export default class MobileSecurity {
readonly preventScreenCaptureToggleFalse: Locator;
readonly jailbreakProtectionToggleTrue: Locator;
readonly jailbreakProtectionToggleFalse: Locator;
+ readonly enableSecureFilePreviewToggleTrue: Locator;
+ readonly enableSecureFilePreviewToggleFalse: Locator;
+ readonly allowPdfLinkNavigationToggleTrue: Locator;
+ readonly allowPdfLinkNavigationToggleFalse: Locator;
readonly saveButton: Locator;
@@ -42,6 +46,27 @@ export default class MobileSecurity {
'NativeAppSettings.MobileJailbreakProtectionfalse',
);
+ this.jailbreakProtectionToggleTrue = this.container.getByTestId(
+ 'NativeAppSettings.MobileJailbreakProtectiontrue',
+ );
+ this.jailbreakProtectionToggleFalse = this.container.getByTestId(
+ 'NativeAppSettings.MobileJailbreakProtectionfalse',
+ );
+
+ this.enableSecureFilePreviewToggleTrue = this.container.getByTestId(
+ 'NativeAppSettings.MobileEnableSecureFilePreviewtrue',
+ );
+ this.enableSecureFilePreviewToggleFalse = this.container.getByTestId(
+ 'NativeAppSettings.MobileEnableSecureFilePreviewfalse',
+ );
+
+ this.allowPdfLinkNavigationToggleTrue = this.container.getByTestId(
+ 'NativeAppSettings.MobileAllowPdfLinkNavigationtrue',
+ );
+ this.allowPdfLinkNavigationToggleFalse = this.container.getByTestId(
+ 'NativeAppSettings.MobileAllowPdfLinkNavigationfalse',
+ );
+
this.saveButton = this.container.getByRole('button', {name: 'Save'});
}
@@ -73,6 +98,22 @@ export default class MobileSecurity {
await this.jailbreakProtectionToggleFalse.click();
}
+ async clickEnableSecureFilePreviewToggleTrue() {
+ await this.enableSecureFilePreviewToggleTrue.click();
+ }
+
+ async clickEnableSecureFilePreviewToggleFalse() {
+ await this.enableSecureFilePreviewToggleFalse.click();
+ }
+
+ async clickAllowPdfLinkNavigationToggleTrue() {
+ await this.allowPdfLinkNavigationToggleTrue.click();
+ }
+
+ async clickAllowPdfLinkNavigationToggleFalse() {
+ await this.allowPdfLinkNavigationToggleFalse.click();
+ }
+
async clickSaveButton() {
await this.saveButton.click();
}
diff --git a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
index af32bc6c50b..d55c92db8c6 100644
--- a/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
+++ b/e2e-tests/playwright/specs/functional/system_console/mobile_security.spec.ts
@@ -8,7 +8,10 @@ test('should be able to enable mobile security settings when licensed', async ({
const license = await adminClient.getClientLicenseOld();
- test.skip(license.SkuShortName !== 'enterprise', 'Skipping test - server has no enterprise license');
+ test.skip(
+ license.SkuShortName !== 'enterprise' || license.short_sku_name !== 'advanced',
+ 'Skipping test - server has no enterprise or enterprise advanced license',
+ );
if (!adminUser) {
throw new Error('Failed to create admin user');
@@ -96,6 +99,62 @@ test('should be able to enable mobile security settings when licensed', async ({
expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true);
expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true);
expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true);
+
+ if (license.SkuShortName === 'advanced') {
+ // # Enable Secure File Preview
+ await systemConsolePage.mobileSecurity.clickEnableSecureFilePreviewToggleTrue();
+
+ // * Verify all toggles are enabled
+ expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(false);
+
+ // # Save settings
+ await systemConsolePage.mobileSecurity.clickSaveButton();
+ // # Wait until the save button has settled
+ await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
+
+ // # Go to any other section and come back to Mobile Security
+ await systemConsolePage.sidebar.goToItem('Users');
+ await systemConsolePage.systemUsers.toBeVisible();
+ await systemConsolePage.sidebar.goToItem('Mobile Security');
+
+ // * Verify all toggles are still enabled
+ expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(false);
+
+ // # Enable Allow PDF Link Navigation
+ await systemConsolePage.mobileSecurity.clickAllowPdfLinkNavigationToggleTrue();
+
+ // * Verify all toggles are enabled
+ expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(true);
+
+ // # Save settings
+ await systemConsolePage.mobileSecurity.clickSaveButton();
+ // # Wait until the save button has settled
+ await pw.waitUntil(async () => (await systemConsolePage.mobileSecurity.saveButton.textContent()) === 'Save');
+
+ // # Go to any other section and come back to Mobile Security
+ await systemConsolePage.sidebar.goToItem('Users');
+ await systemConsolePage.systemUsers.toBeVisible();
+ await systemConsolePage.sidebar.goToItem('Mobile Security');
+
+ // * Verify all toggles are still enabled
+ expect(await systemConsolePage.mobileSecurity.enableBiometricAuthenticationToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.preventScreenCaptureToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.jailbreakProtectionToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.enableSecureFilePreviewToggleTrue.isChecked()).toBe(true);
+ expect(await systemConsolePage.mobileSecurity.allowPdfLinkNavigationToggleTrue.isChecked()).toBe(true);
+ }
});
test('should show mobile security upsell when not licensed', async ({pw}) => {
diff --git a/server/config/client.go b/server/config/client.go
index fa96bca1abb..7ee4e7b4f0c 100644
--- a/server/config/client.go
+++ b/server/config/client.go
@@ -409,6 +409,11 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["MobilePreventScreenCapture"] = strconv.FormatBool(*c.NativeAppSettings.MobilePreventScreenCapture)
props["MobileJailbreakProtection"] = strconv.FormatBool(*c.NativeAppSettings.MobileJailbreakProtection)
}
+
+ if model.MinimumEnterpriseAdvancedLicense(license) {
+ props["MobileEnableSecureFilePreview"] = strconv.FormatBool(*c.NativeAppSettings.MobileEnableSecureFilePreview)
+ props["MobileAllowPdfLinkNavigation"] = strconv.FormatBool(*c.NativeAppSettings.MobileAllowPdfLinkNavigation)
+ }
}
for key, value := range c.FeatureFlags.ToMap() {
diff --git a/server/public/model/config.go b/server/public/model/config.go
index cdd944a5402..420273bdc13 100644
--- a/server/public/model/config.go
+++ b/server/public/model/config.go
@@ -2906,14 +2906,16 @@ func (s *SamlSettings) SetDefaults() {
}
type NativeAppSettings struct {
- AppCustomURLSchemes []string `access:"site_customization,write_restrictable,cloud_restrictable"` // telemetry: none
- AppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
- AndroidAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
- IosAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
- MobileExternalBrowser *bool `access:"site_customization,write_restrictable,cloud_restrictable"`
- MobileEnableBiometrics *bool `access:"site_customization,write_restrictable"`
- MobilePreventScreenCapture *bool `access:"site_customization,write_restrictable"`
- MobileJailbreakProtection *bool `access:"site_customization,write_restrictable"`
+ AppCustomURLSchemes []string `access:"site_customization,write_restrictable,cloud_restrictable"` // telemetry: none
+ AppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
+ AndroidAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
+ IosAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
+ MobileExternalBrowser *bool `access:"site_customization,write_restrictable,cloud_restrictable"`
+ MobileEnableBiometrics *bool `access:"site_customization,write_restrictable"`
+ MobilePreventScreenCapture *bool `access:"site_customization,write_restrictable"`
+ MobileJailbreakProtection *bool `access:"site_customization,write_restrictable"`
+ MobileEnableSecureFilePreview *bool `access:"site_customization,write_restrictable"`
+ MobileAllowPdfLinkNavigation *bool `access:"site_customization,write_restrictable"`
}
func (s *NativeAppSettings) SetDefaults() {
@@ -2948,6 +2950,14 @@ func (s *NativeAppSettings) SetDefaults() {
if s.MobileJailbreakProtection == nil {
s.MobileJailbreakProtection = NewPointer(false)
}
+
+ if s.MobileEnableSecureFilePreview == nil {
+ s.MobileEnableSecureFilePreview = NewPointer(false)
+ }
+
+ if s.MobileAllowPdfLinkNavigation == nil {
+ s.MobileAllowPdfLinkNavigation = NewPointer(false)
+ }
}
type ElasticsearchSettings struct {
diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx
index 6e426f88f81..117e0ecf4be 100644
--- a/webapp/channels/src/components/admin_console/admin_definition.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition.tsx
@@ -2130,6 +2130,33 @@ const AdminDefinition: AdminDefinitionType = {
label: defineMessage({id: 'admin.mobileSecurity.jailbreakTitle', defaultMessage: 'Enable Jailbreak/Root Protection:'}),
help_text: defineMessage({id: 'admin.mobileSecurity.jailbreakDescription', defaultMessage: '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.'}),
},
+ {
+ type: 'bool',
+ key: 'NativeAppSettings.MobileEnableSecureFilePreview',
+ label: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewTitle', defaultMessage: 'Enable Secure File Preview Mode:'}),
+ help_text: defineMessage({id: 'admin.mobileSecurity.secureFilePreviewDescription', defaultMessage: '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.'}),
+ help_text_values: {
+ mobileAllowDownloads: (
+
+
+
+
+
+ ),
+ },
+ isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
+ },
+ {
+ type: 'bool',
+ key: 'NativeAppSettings.MobileAllowPdfLinkNavigation',
+ label: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationTitle', defaultMessage: 'Allow Link Navigation in Secure PDFs:'}),
+ help_text: defineMessage({id: 'admin.mobileSecurity.allowPdfLinkNavigationDescription', defaultMessage: 'Enables tapping links inside PDFs when Secure File Preview Mode is active. Links will open in the device browser or supported app. Has no effect when Secure File Preview Mode is disabled.'}),
+ isDisabled: it.stateIsFalse('NativeAppSettings.MobileEnableSecureFilePreview'),
+ isHidden: it.not(it.minLicenseTier(LicenseSkus.EnterpriseAdvanced)),
+ },
],
},
},
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index a103366a27f..5d2a84cfc0d 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -1729,12 +1729,17 @@
"admin.mfa.bannerDesc": "Multi-factor authentication is available for accounts with AD/LDAP or email login. If other login methods are used, MFA should be configured with the authentication provider.",
"admin.mobile_security_feature_discovery.copy": "Enable advanced security features like biometric authentication, screen capture prevention, and jailbreak/root detection for your mobile users.",
"admin.mobile_security_feature_discovery.title": "Enhance mobile app security with Mattermost Enterprise",
+ "admin.mobileSecurity.allowPdfLinkNavigationDescription": "Enables tapping links inside PDFs when Secure File Preview Mode is active. Links will open in the device browser or supported app. Has no effect when Secure File Preview Mode is disabled.",
+ "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.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.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.",
+ "admin.mobileSecurity.secureFilePreviewTitle": "Enable Secure File Preview Mode:",
"admin.mobileSecurity.title": "Mobile Security",
"admin.nav.administratorsGuide": "Administrator's Guide",
"admin.nav.commercialSupport": "Commercial Support",
diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts
index 8bb163b14bb..962b14dd29d 100644
--- a/webapp/platform/types/src/config.ts
+++ b/webapp/platform/types/src/config.ts
@@ -787,6 +787,8 @@ export type NativeAppSettings = {
MobileEnableBiometrics: boolean;
MobilePreventScreenCapture: boolean;
MobileJailbreakProtection: boolean;
+ MobileEnableSecureFilePreview: boolean;
+ MobileAllowPdfLinkNavigation: boolean;
};
export type ClusterSettings = {