* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
835 lines
39 KiB
TypeScript
835 lines
39 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/* eslint-disable max-lines */
|
|
|
|
import Emm from '@mattermost/react-native-emm';
|
|
import {isRootedExperimentalAsync} from 'expo-device';
|
|
import {type AppStateStatus} from 'react-native';
|
|
|
|
import DatabaseManager from '@database/manager';
|
|
import {getServerCredentials} from '@init/credentials';
|
|
import IntuneManager from '@managers/intune_manager';
|
|
import {
|
|
IntuneConditionalLaunchBlockedReasons,
|
|
type IntuneAuthRequiredEvent,
|
|
type IntuneConditionalLaunchBlockedEvent,
|
|
type IntuneEnrollmentChangedEvent,
|
|
type IntuneIdentitySwitchRequiredEvent,
|
|
type IntunePolicy,
|
|
type IntunePolicyChangedEvent,
|
|
type IntuneWipeRequestedEvent,
|
|
} from '@managers/intune_manager/types';
|
|
import TestHelper from '@test/test_helper';
|
|
import * as alerts from '@utils/alerts';
|
|
import {toMilliseconds} from '@utils/datetime';
|
|
|
|
import SecurityManager from '.';
|
|
|
|
jest.mock('@mattermost/react-native-emm', () => ({
|
|
isDeviceSecured: jest.fn(),
|
|
authenticate: jest.fn(),
|
|
openSecuritySettings: jest.fn(),
|
|
exitApp: jest.fn(),
|
|
enableBlurScreen: jest.fn(),
|
|
applyBlurEffect: jest.fn(),
|
|
removeBlurEffect: jest.fn(),
|
|
addListener: jest.fn(),
|
|
setAppGroupId: jest.fn(),
|
|
getManagedConfig: jest.fn(() => ({})),
|
|
}));
|
|
|
|
jest.mock('expo-device', () => ({
|
|
isRootedExperimentalAsync: jest.fn(),
|
|
}));
|
|
jest.mock('@actions/local/session', () => ({terminateSession: jest.fn(() => Promise.resolve())}));
|
|
jest.mock('@actions/local/user', () => ({getCurrentUserLocale: jest.fn(() => Promise.resolve('en'))}));
|
|
jest.mock('@actions/remote/session', () => ({
|
|
logout: jest.fn(() => Promise.resolve()),
|
|
}));
|
|
jest.mock('@utils/log', () => ({
|
|
logError: jest.fn(),
|
|
logDebug: jest.fn(),
|
|
}));
|
|
jest.mock('@utils/datetime', () => ({toMilliseconds: jest.fn(() => 25000)}));
|
|
jest.mock('@init/credentials', () => ({getServerCredentials: jest.fn().mockResolvedValue({token: 'token'})}));
|
|
jest.mock('@database/manager', () => ({
|
|
getActiveServerUrl: jest.fn(),
|
|
getServerDatabaseAndOperator: jest.fn(),
|
|
}));
|
|
|
|
describe('SecurityManager - Event Handlers', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
SecurityManager.initialized = false;
|
|
SecurityManager.serverConfig = {};
|
|
SecurityManager.activeServer = undefined;
|
|
});
|
|
|
|
describe('onAppStateChange', () => {
|
|
test('should handle app state changes', async () => {
|
|
await SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as SecurityClientConfig);
|
|
await SecurityManager.setActiveServer({serverUrl: 'server-8'});
|
|
await SecurityManager.onAppStateChange('background' as AppStateStatus);
|
|
expect(SecurityManager.backgroundSince).toBeGreaterThan(0);
|
|
await SecurityManager.onAppStateChange('active' as AppStateStatus);
|
|
expect(SecurityManager.backgroundSince).toBe(0);
|
|
});
|
|
|
|
test('should call biometric authentication app state changes', async () => {
|
|
const authenticateWithBiometrics = jest.spyOn(SecurityManager, 'authenticateWithBiometrics');
|
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(false);
|
|
await SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as SecurityClientConfig);
|
|
await SecurityManager.setActiveServer({serverUrl: 'server-8'});
|
|
SecurityManager.onAppStateChange('background' as AppStateStatus);
|
|
SecurityManager.backgroundSince = Date.now() - toMilliseconds({minutes: 5, seconds: 1});
|
|
SecurityManager.onAppStateChange('active' as AppStateStatus);
|
|
await TestHelper.wait(300);
|
|
expect(authenticateWithBiometrics).toHaveBeenCalledWith('server-8');
|
|
});
|
|
});
|
|
|
|
describe('Intune Event Handlers', () => {
|
|
const serverUrl1 = 'https://test1.server.com';
|
|
const serverUrl2 = 'https://test2.server.com';
|
|
const mockPolicy: IntunePolicy = {
|
|
isPINRequired: true,
|
|
isContactSyncAllowed: false,
|
|
isWidgetContentSyncAllowed: false,
|
|
isSpotlightIndexingAllowed: false,
|
|
areSiriIntentsAllowed: false,
|
|
areAppIntentsAllowed: false,
|
|
isAppSharingAllowed: false,
|
|
shouldFileProviderEncryptFiles: true,
|
|
isManagedBrowserRequired: false,
|
|
isFileEncryptionRequired: true,
|
|
isScreenCaptureAllowed: false,
|
|
notificationPolicy: 0,
|
|
allowedSaveLocations: {
|
|
Other: false,
|
|
OneDriveForBusiness: true,
|
|
SharePoint: true,
|
|
LocalDrive: false,
|
|
PhotoLibrary: false,
|
|
CameraRoll: false,
|
|
FilesApp: false,
|
|
iCloudDrive: false,
|
|
},
|
|
allowedOpenLocations: 0,
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
await SecurityManager.init();
|
|
await SecurityManager.addServer(serverUrl1, {SiteName: 'Test Server 1'} as SecurityClientConfig);
|
|
await SecurityManager.addServer(serverUrl2, {SiteName: 'Test Server 2'} as SecurityClientConfig);
|
|
});
|
|
|
|
afterEach(() => {
|
|
SecurityManager.cleanup();
|
|
});
|
|
|
|
describe('onIntunePolicyChanged', () => {
|
|
test('should skip if not initialized', () => {
|
|
SecurityManager.cleanup();
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: true,
|
|
removed: false,
|
|
policy: mockPolicy,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
// After cleanup, handler should return early without modifying config
|
|
// Server still exists with null policy (from addServer)
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.intunePolicy).toBeUndefined();
|
|
});
|
|
|
|
test('should update policy for affected servers when policy changed', () => {
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: true,
|
|
removed: false,
|
|
policy: mockPolicy,
|
|
serverUrls: [serverUrl1, serverUrl2],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
const config1 = SecurityManager.getServerConfig(serverUrl1);
|
|
const config2 = SecurityManager.getServerConfig(serverUrl2);
|
|
expect(config1?.intunePolicy).toEqual(mockPolicy);
|
|
expect(config2?.intunePolicy).toEqual(mockPolicy);
|
|
});
|
|
|
|
test('should remove policy when policy removed', () => {
|
|
// First set a policy
|
|
SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
|
|
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: false,
|
|
removed: true,
|
|
policy: null,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.intunePolicy).toBeNull();
|
|
});
|
|
|
|
test('should re-apply screen capture policy when active server affected', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: true,
|
|
removed: false,
|
|
policy: mockPolicy,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should not re-apply when active server not affected', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
setScreenCaptureSpy.mockClear();
|
|
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: true,
|
|
removed: false,
|
|
policy: mockPolicy,
|
|
serverUrls: [serverUrl2],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
// Should not be called again for serverUrl1
|
|
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should handle multiple servers in event', () => {
|
|
const event: IntunePolicyChangedEvent = {
|
|
oid: 'object-id',
|
|
changed: true,
|
|
removed: false,
|
|
policy: mockPolicy,
|
|
serverUrls: [serverUrl1, serverUrl2],
|
|
};
|
|
|
|
SecurityManager.onIntunePolicyChanged(event);
|
|
|
|
const config1 = SecurityManager.getServerConfig(serverUrl1);
|
|
const config2 = SecurityManager.getServerConfig(serverUrl2);
|
|
expect(config1?.intunePolicy).toEqual(mockPolicy);
|
|
expect(config2?.intunePolicy).toEqual(mockPolicy);
|
|
});
|
|
});
|
|
|
|
describe('onEnrollmentChanged', () => {
|
|
test('should call handleEnrollmentSuccess when enrolled', async () => {
|
|
const handleSuccessSpy = jest.spyOn(SecurityManager as any, 'handleEnrollmentSuccess');
|
|
const event: IntuneEnrollmentChangedEvent = {
|
|
oid: 'object-id',
|
|
enrolled: true,
|
|
reason: 'user_enrolled',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
SecurityManager.onEnrollmentChanged(event);
|
|
|
|
await TestHelper.wait(10);
|
|
expect(handleSuccessSpy).toHaveBeenCalledWith([serverUrl1]);
|
|
handleSuccessSpy.mockRestore();
|
|
});
|
|
|
|
test('should call handleUnenrollment when not enrolled', async () => {
|
|
const handleUnenrollmentSpy = jest.spyOn(SecurityManager as any, 'handleUnenrollment');
|
|
const event: IntuneEnrollmentChangedEvent = {
|
|
oid: 'object-id',
|
|
enrolled: false,
|
|
reason: 'user_unenrolled',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
SecurityManager.onEnrollmentChanged(event);
|
|
|
|
await TestHelper.wait(10);
|
|
expect(handleUnenrollmentSpy).toHaveBeenCalledWith([serverUrl1], 'user_unenrolled');
|
|
handleUnenrollmentSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onWipeRequested', () => {
|
|
beforeEach(() => {
|
|
jest.mocked(getServerCredentials).mockResolvedValue({serverUrl: serverUrl1, userId: 'user-id', token: 'token'});
|
|
});
|
|
|
|
test('should wipe background servers first, then active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneWipeRequestedEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1, serverUrl2],
|
|
};
|
|
|
|
await SecurityManager.onWipeRequested(event);
|
|
|
|
// Background server wiped first with skipEvents=true
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl2, true);
|
|
|
|
// Active server wiped last with skipEvents=false
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, false);
|
|
|
|
performWipeSpy.mockRestore();
|
|
});
|
|
|
|
test('should proceed with wipe even without credentials (already deleted by native)', async () => {
|
|
jest.mocked(getServerCredentials).mockResolvedValue(null);
|
|
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(true);
|
|
|
|
const event: IntuneWipeRequestedEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onWipeRequested(event);
|
|
|
|
// Should still call wipe to clean up database (credentials already deleted by native)
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
|
|
performWipeSpy.mockRestore();
|
|
});
|
|
|
|
test('should remove active server after wipe', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
|
|
const removeServerSpy = jest.spyOn(SecurityManager, 'removeServer');
|
|
|
|
const event: IntuneWipeRequestedEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onWipeRequested(event);
|
|
|
|
expect(removeServerSpy).toHaveBeenCalledWith(serverUrl1);
|
|
removeServerSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onAuthRequired', () => {
|
|
test('should apply blur screen and trigger wipe', async () => {
|
|
const showAlertSpy = jest.spyOn(alerts, 'showAuthenticationRequiredAlert').mockImplementation(async (_reason, _locale, onDismiss) => {
|
|
if (onDismiss) {
|
|
onDismiss();
|
|
}
|
|
});
|
|
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneAuthRequiredEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onAuthRequired(event);
|
|
|
|
expect(Emm.applyBlurEffect).toHaveBeenCalled();
|
|
expect(showAlertSpy).toHaveBeenCalled();
|
|
await TestHelper.wait(10);
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
|
|
expect(Emm.removeBlurEffect).toHaveBeenCalled();
|
|
|
|
showAlertSpy.mockRestore();
|
|
performWipeSpy.mockRestore();
|
|
});
|
|
|
|
test('should remove blur screen when alert dismissed', async () => {
|
|
const showAlertSpy = jest.spyOn(alerts, 'showAuthenticationRequiredAlert').mockImplementation(async (_reason, _locale, onDismiss) => {
|
|
if (onDismiss) {
|
|
onDismiss();
|
|
}
|
|
});
|
|
|
|
const event: IntuneAuthRequiredEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onAuthRequired(event);
|
|
|
|
expect(Emm.applyBlurEffect).toHaveBeenCalled();
|
|
await TestHelper.wait(10);
|
|
expect(Emm.removeBlurEffect).toHaveBeenCalled();
|
|
|
|
showAlertSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onConditionalLaunchBlocked', () => {
|
|
test('should trigger wipe when launch blocked', async () => {
|
|
const showAlertSpy = jest.spyOn(alerts, 'showConditionalAccessAlert').mockImplementation(async (_locale, callback) => {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneConditionalLaunchBlockedEvent = {
|
|
oid: 'object-id',
|
|
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_BLOCKED,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onConditionalLaunchBlocked(event);
|
|
|
|
expect(Emm.applyBlurEffect).toHaveBeenCalled();
|
|
expect(showAlertSpy).toHaveBeenCalled();
|
|
await TestHelper.wait(10);
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
|
|
|
|
showAlertSpy.mockRestore();
|
|
performWipeSpy.mockRestore();
|
|
});
|
|
|
|
test('should remove blur screen when launch blocked alert dismissed', async () => {
|
|
const showAlertSpy = jest.spyOn(alerts, 'showConditionalAccessAlert').mockImplementation(async (_locale, callback) => {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
const event: IntuneConditionalLaunchBlockedEvent = {
|
|
oid: 'object-id',
|
|
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_BLOCKED,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onConditionalLaunchBlocked(event);
|
|
|
|
await TestHelper.wait(10);
|
|
expect(Emm.removeBlurEffect).toHaveBeenCalled();
|
|
|
|
showAlertSpy.mockRestore();
|
|
});
|
|
|
|
test('should retry authentication when launch canceled', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
|
|
const showAlertSpy = jest.spyOn(alerts, 'showBiometricFailureAlertForOrganization').mockImplementation(async (_server, _locale, retryCallback) => {
|
|
if (retryCallback) {
|
|
await retryCallback();
|
|
}
|
|
});
|
|
const setIdentitySpy = jest.spyOn(IntuneManager, 'setCurrentIdentity').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneConditionalLaunchBlockedEvent = {
|
|
oid: 'object-id',
|
|
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_CANCELED,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onConditionalLaunchBlocked(event);
|
|
|
|
expect(showAlertSpy).toHaveBeenCalled();
|
|
expect(setIdentitySpy).toHaveBeenCalledWith(serverUrl1);
|
|
|
|
showAlertSpy.mockRestore();
|
|
setIdentitySpy.mockRestore();
|
|
});
|
|
|
|
test('should remove blur screen after retry', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
|
|
const showAlertSpy = jest.spyOn(alerts, 'showBiometricFailureAlertForOrganization').mockImplementation(async (_server, _locale, retryCallback) => {
|
|
if (retryCallback) {
|
|
await retryCallback();
|
|
}
|
|
});
|
|
jest.spyOn(IntuneManager, 'setCurrentIdentity').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneConditionalLaunchBlockedEvent = {
|
|
oid: 'object-id',
|
|
reason: IntuneConditionalLaunchBlockedReasons.LAUNCH_CANCELED,
|
|
serverUrls: [serverUrl1],
|
|
};
|
|
|
|
await SecurityManager.onConditionalLaunchBlocked(event);
|
|
|
|
expect(Emm.removeBlurEffect).toHaveBeenCalled();
|
|
|
|
showAlertSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onIdentitySwitchRequired', () => {
|
|
test('should trigger wipe and show alert', async () => {
|
|
const showAlertSpy = jest.spyOn(alerts, 'showIdentitySwitchRequiredAlert').mockResolvedValue(undefined);
|
|
const performWipeSpy = jest.spyOn(SecurityManager as any, 'performSelectiveWipe').mockResolvedValue(undefined);
|
|
|
|
const event: IntuneIdentitySwitchRequiredEvent = {
|
|
oid: 'object-id',
|
|
serverUrls: [serverUrl1],
|
|
reason: 'new_identity_required',
|
|
};
|
|
|
|
await SecurityManager.onIdentitySwitchRequired(event);
|
|
|
|
expect(showAlertSpy).toHaveBeenCalled();
|
|
await TestHelper.wait(10);
|
|
expect(performWipeSpy).toHaveBeenCalledWith(serverUrl1, true);
|
|
|
|
showAlertSpy.mockRestore();
|
|
performWipeSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('handleEnrollmentSuccess', () => {
|
|
beforeEach(() => {
|
|
jest.mocked(DatabaseManager.getActiveServerUrl).mockResolvedValue(serverUrl1);
|
|
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
|
|
});
|
|
|
|
test('should fetch and update policy for all affected servers', async () => {
|
|
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1, serverUrl2]);
|
|
|
|
const config1 = SecurityManager.getServerConfig(serverUrl1);
|
|
const config2 = SecurityManager.getServerConfig(serverUrl2);
|
|
expect(config1?.intunePolicy).toEqual(mockPolicy);
|
|
expect(config2?.intunePolicy).toEqual(mockPolicy);
|
|
});
|
|
|
|
test('should set current identity if active server is affected', async () => {
|
|
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1, serverUrl2]);
|
|
|
|
expect(IntuneManager.setCurrentIdentity).toHaveBeenCalledWith(serverUrl1);
|
|
});
|
|
|
|
test('should re-apply screen capture policy for active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
|
|
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl1]);
|
|
|
|
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should not set current identity if active server not affected', async () => {
|
|
jest.mocked(IntuneManager.setCurrentIdentity).mockClear();
|
|
|
|
await (SecurityManager as any).handleEnrollmentSuccess([serverUrl2]);
|
|
|
|
expect(IntuneManager.setCurrentIdentity).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleUnenrollment', () => {
|
|
beforeEach(async () => {
|
|
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
|
|
await SecurityManager.addServer(serverUrl2, {} as SecurityClientConfig, false, mockPolicy);
|
|
jest.mocked(DatabaseManager.getActiveServerUrl).mockResolvedValue(serverUrl1);
|
|
});
|
|
|
|
test('should remove intunePolicy from all affected servers', async () => {
|
|
await (SecurityManager as any).handleUnenrollment([serverUrl1, serverUrl2], 'admin_unenrolled');
|
|
|
|
const config1 = SecurityManager.getServerConfig(serverUrl1);
|
|
const config2 = SecurityManager.getServerConfig(serverUrl2);
|
|
expect(config1?.intunePolicy).toBeNull();
|
|
expect(config2?.intunePolicy).toBeNull();
|
|
});
|
|
|
|
test('should re-apply screen capture policy if active server affected', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
|
|
await (SecurityManager as any).handleUnenrollment([serverUrl1], 'policy_removed');
|
|
|
|
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should not re-apply if active server not affected', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
setScreenCaptureSpy.mockClear();
|
|
|
|
await (SecurityManager as any).handleUnenrollment([serverUrl2]);
|
|
|
|
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onConfigChanged', () => {
|
|
const mockConfig = {
|
|
SiteName: 'Updated Server',
|
|
MobileEnableBiometrics: 'true',
|
|
MobileJailbreakProtection: 'true',
|
|
MobilePreventScreenCapture: 'true',
|
|
} as SecurityClientConfig;
|
|
|
|
beforeEach(() => {
|
|
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
|
|
});
|
|
|
|
test('should update server config when config changes', async () => {
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
|
|
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.siteName).toBe('Updated Server');
|
|
expect(config?.Biometrics).toBe(true);
|
|
expect(config?.JailbreakProtection).toBe(true);
|
|
expect(config?.PreventScreenCapture).toBe(true);
|
|
});
|
|
|
|
test('should fetch and store Intune policy when config changes', async () => {
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
|
|
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.intunePolicy).toEqual(mockPolicy);
|
|
expect(IntuneManager.getPolicy).toHaveBeenCalledWith(serverUrl1);
|
|
});
|
|
|
|
test('should preserve existing config fields when updating', async () => {
|
|
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, true);
|
|
SecurityManager.serverConfig[serverUrl1].lastAccessed = 12345;
|
|
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
|
|
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.lastAccessed).toBe(12345);
|
|
expect(config?.authenticated).toBe(true);
|
|
});
|
|
|
|
test('should trigger MAM enrollment check for active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
|
|
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
|
|
|
|
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
|
|
test('should apply screen capture policy for active server after enrollment check', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl1, config: mockConfig});
|
|
|
|
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should not trigger enrollment for non-active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
|
|
|
|
await SecurityManager.onConfigChanged({serverUrl: serverUrl2, config: mockConfig});
|
|
|
|
expect(ensureMAMSpy).not.toHaveBeenCalled();
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('onLicenseChanged', () => {
|
|
const mockLicense = {
|
|
IsLicensed: 'true',
|
|
SkuShortName: 'enterprise',
|
|
} as ClientLicense;
|
|
|
|
beforeEach(() => {
|
|
jest.mocked(IntuneManager.getPolicy).mockResolvedValue(mockPolicy);
|
|
});
|
|
|
|
test('should create server config if not exists', async () => {
|
|
await SecurityManager.onLicenseChanged({serverUrl: 'https://new-server.com', license: mockLicense});
|
|
|
|
const config = SecurityManager.getServerConfig('https://new-server.com');
|
|
expect(config).toBeDefined();
|
|
expect(config?.intunePolicy).toEqual(mockPolicy);
|
|
});
|
|
|
|
test('should fetch and update Intune policy when license changes', async () => {
|
|
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
|
|
|
|
const config = SecurityManager.getServerConfig(serverUrl1);
|
|
expect(config?.intunePolicy).toEqual(mockPolicy);
|
|
expect(IntuneManager.getPolicy).toHaveBeenCalledWith(serverUrl1);
|
|
});
|
|
|
|
test('should trigger MAM enrollment check for active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
|
|
|
|
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
|
|
|
|
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
|
|
test('should set current identity for active server after successful enrollment', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
|
|
|
|
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
|
|
|
|
expect(IntuneManager.setCurrentIdentity).toHaveBeenCalledWith(serverUrl1);
|
|
});
|
|
|
|
test('should not set identity if enrollment fails', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(false);
|
|
jest.mocked(IntuneManager.setCurrentIdentity).mockClear();
|
|
|
|
await SecurityManager.onLicenseChanged({serverUrl: serverUrl1, license: mockLicense});
|
|
|
|
expect(IntuneManager.setCurrentIdentity).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should not trigger enrollment for non-active server', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
|
|
|
|
await SecurityManager.onLicenseChanged({serverUrl: serverUrl2, license: mockLicense});
|
|
|
|
expect(ensureMAMSpy).not.toHaveBeenCalled();
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('canSaveToLocation', () => {
|
|
test('should return true when no Intune policy exists', () => {
|
|
const result = SecurityManager.canSaveToLocation(serverUrl1, 'PhotoLibrary');
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test('should return policy value when Intune policy exists', async () => {
|
|
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
|
|
|
|
const canSaveToOneDrive = SecurityManager.canSaveToLocation(serverUrl1, 'OneDriveForBusiness');
|
|
const canSaveToPhotoLibrary = SecurityManager.canSaveToLocation(serverUrl1, 'PhotoLibrary');
|
|
|
|
expect(canSaveToOneDrive).toBe(true);
|
|
expect(canSaveToPhotoLibrary).toBe(false);
|
|
});
|
|
|
|
test('should return false for restricted locations', async () => {
|
|
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
|
|
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'LocalDrive')).toBe(false);
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'CameraRoll')).toBe(false);
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'FilesApp')).toBe(false);
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'iCloudDrive')).toBe(false);
|
|
});
|
|
|
|
test('should return true for allowed locations', async () => {
|
|
await SecurityManager.addServer(serverUrl1, {} as SecurityClientConfig, false, mockPolicy);
|
|
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'OneDriveForBusiness')).toBe(true);
|
|
expect(SecurityManager.canSaveToLocation(serverUrl1, 'SharePoint')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('setActiveServer options', () => {
|
|
beforeEach(async () => {
|
|
jest.mocked(IntuneManager.isIntuneMAMEnabledForServer).mockResolvedValue(true);
|
|
jest.mocked(IntuneManager.isManagedServer).mockResolvedValue(false);
|
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(true);
|
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
|
jest.mocked(Emm.authenticate).mockResolvedValue(true);
|
|
});
|
|
|
|
test('should skip MAM enrollment check when skipMAMEnrollmentCheck is true', async () => {
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer');
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true}});
|
|
|
|
expect(ensureMAMSpy).not.toHaveBeenCalled();
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
|
|
test('should perform MAM enrollment check when skipMAMEnrollmentCheck is false', async () => {
|
|
const ensureMAMSpy = jest.spyOn(SecurityManager, 'ensureMAMEnrollmentForActiveServer').mockResolvedValue(true);
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: false}});
|
|
|
|
expect(ensureMAMSpy).toHaveBeenCalledWith(serverUrl1);
|
|
ensureMAMSpy.mockRestore();
|
|
});
|
|
|
|
test('should skip jailbreak check when skipJailbreakCheck is true', async () => {
|
|
const jailbreakSpy = jest.spyOn(SecurityManager, 'isDeviceJailbroken');
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true}});
|
|
|
|
expect(jailbreakSpy).not.toHaveBeenCalled();
|
|
jailbreakSpy.mockRestore();
|
|
});
|
|
|
|
test('should perform jailbreak check when skipJailbreakCheck is false', async () => {
|
|
const jailbreakSpy = jest.spyOn(SecurityManager, 'isDeviceJailbroken').mockResolvedValue(false);
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: false}});
|
|
|
|
expect(jailbreakSpy).toHaveBeenCalledWith(serverUrl1);
|
|
jailbreakSpy.mockRestore();
|
|
});
|
|
|
|
test('should skip biometric auth when skipBiometricCheck is true', async () => {
|
|
const biometricSpy = jest.spyOn(SecurityManager, 'authenticateWithBiometricsIfNeeded');
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
|
|
expect(biometricSpy).not.toHaveBeenCalled();
|
|
biometricSpy.mockRestore();
|
|
});
|
|
|
|
test('should perform biometric auth when skipBiometricCheck is false', async () => {
|
|
const biometricSpy = jest.spyOn(SecurityManager, 'authenticateWithBiometricsIfNeeded');
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: false}});
|
|
|
|
expect(biometricSpy).toHaveBeenCalledWith(serverUrl1);
|
|
biometricSpy.mockRestore();
|
|
});
|
|
|
|
test('should force switch to same server when forceSwitch is true', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
setScreenCaptureSpy.mockClear();
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true, forceSwitch: true}});
|
|
|
|
expect(setScreenCaptureSpy).toHaveBeenCalledWith(serverUrl1);
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
|
|
test('should not switch to same server when forceSwitch is false', async () => {
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true}});
|
|
const setScreenCaptureSpy = jest.spyOn(SecurityManager, 'setScreenCapturePolicy');
|
|
setScreenCaptureSpy.mockClear();
|
|
|
|
await SecurityManager.setActiveServer({serverUrl: serverUrl1, options: {skipMAMEnrollmentCheck: true, skipJailbreakCheck: true, skipBiometricCheck: true, forceSwitch: false}});
|
|
|
|
expect(setScreenCaptureSpy).not.toHaveBeenCalled();
|
|
setScreenCaptureSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|
|
});
|