diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts index 41674f5d57..e601d58d75 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 8bb78d2df2..e5017348c1 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 af32bc6c50..d55c92db8c 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 fa96bca1ab..7ee4e7b4f0 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 cdd944a540..420273bdc1 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 6e426f88f8..117e0ecf4b 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 a103366a27..5d2a84cfc0 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 8bb163b14b..962b14dd29 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 = {