* 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>
190 lines
6.7 KiB
TypeScript
190 lines
6.7 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import NetInfo from '@react-native-community/netinfo';
|
|
import {Platform} from 'react-native';
|
|
|
|
import {removePushDisabledInServerAcknowledged} from '@actions/app/global';
|
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
|
import DatabaseManager from '@database/manager';
|
|
import {resetMomentLocale} from '@i18n';
|
|
import {getAllServerCredentials, removeServerCredentials} from '@init/credentials';
|
|
import PushNotifications from '@init/push_notifications';
|
|
import NetworkManager from '@managers/network_manager';
|
|
import WebsocketManager from '@managers/websocket_manager';
|
|
import {getDeviceToken} from '@queries/app/global';
|
|
import {getExpiredSession} from '@queries/servers/system';
|
|
import {getCurrentUser} from '@queries/servers/user';
|
|
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
|
import {logError, logWarning} from '@utils/log';
|
|
import {clearCookiesForServer, getCSRFFromCookie, urlSafeBase64Encode} from '@utils/security';
|
|
|
|
const resetLocale = async () => {
|
|
if (Object.keys(DatabaseManager.serverDatabases).length) {
|
|
const serverDatabase = await DatabaseManager.getActiveServerDatabase();
|
|
const user = await getCurrentUser(serverDatabase!);
|
|
resetMomentLocale(user?.locale);
|
|
} else {
|
|
resetMomentLocale();
|
|
}
|
|
};
|
|
|
|
export async function findSession(serverUrl: string, sessions: Session[]) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const expiredSession = await getExpiredSession(database);
|
|
const deviceToken = await getDeviceToken();
|
|
|
|
// First try and find the session by the given identifier
|
|
let session = sessions.find((s) => s.id === expiredSession?.id);
|
|
if (session) {
|
|
return session;
|
|
}
|
|
|
|
// Next try and find the session by deviceId
|
|
if (deviceToken) {
|
|
session = sessions.find((s) => s.device_id === deviceToken);
|
|
if (session) {
|
|
return session;
|
|
}
|
|
}
|
|
|
|
// Next try and find the session by the CSRF token
|
|
const csrfToken = await getCSRFFromCookie(serverUrl);
|
|
if (csrfToken) {
|
|
session = sessions.find((s) => s.props?.csrf === csrfToken);
|
|
if (session) {
|
|
return session;
|
|
}
|
|
}
|
|
|
|
// Next try and find the session based on the OS
|
|
// if multiple sessions exists with the same os type this can be inaccurate
|
|
session = sessions.find((s) => s.props?.os.toLowerCase() === Platform.OS);
|
|
if (session) {
|
|
return session;
|
|
}
|
|
} catch (e) {
|
|
logError('findSession', e);
|
|
}
|
|
|
|
// At this point we did not find the session
|
|
return undefined;
|
|
}
|
|
|
|
export const cancelAllSessionNotifications = async () => {
|
|
const serverCredentials = await getAllServerCredentials();
|
|
for (const {serverUrl} of serverCredentials) {
|
|
cancelSessionNotification(serverUrl);
|
|
}
|
|
};
|
|
|
|
export const cancelSessionNotification = async (serverUrl: string) => {
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const expiredSession = await getExpiredSession(database);
|
|
const rechable = (await NetInfo.fetch()).isInternetReachable;
|
|
|
|
if (expiredSession?.notificationId && rechable) {
|
|
PushNotifications.cancelScheduleNotification(parseInt(expiredSession.notificationId, 10));
|
|
operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
|
value: '',
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
}
|
|
|
|
return {};
|
|
} catch (e) {
|
|
logError('cancelSessionNotification', e);
|
|
return {error: e};
|
|
}
|
|
};
|
|
|
|
export const terminateSession = async (serverUrl: string, removeServer: boolean) => {
|
|
const errors: Array<{operation: string; error: unknown}> = [];
|
|
|
|
// Helper to safely execute operations and optionally track errors
|
|
const safeExecute = async (operation: string, fn: () => Promise<unknown>, critical = true) => {
|
|
try {
|
|
const result = await fn();
|
|
|
|
// Check if function returned {error}
|
|
if (result && typeof result === 'object' && 'error' in result && critical) {
|
|
errors.push({operation, error: result.error});
|
|
}
|
|
} catch (error) {
|
|
if (critical) {
|
|
errors.push({operation, error});
|
|
} else {
|
|
// Log but don't track as failure
|
|
logWarning(`terminateSession: ${operation} failed (non-critical)`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Cancel session notification (critical)
|
|
await safeExecute('cancelSessionNotification', async () => {
|
|
await cancelSessionNotification(serverUrl);
|
|
}, false);
|
|
|
|
// Remove server credentials (critical)
|
|
await safeExecute('removeServerCredentials', async () => {
|
|
await removeServerCredentials(serverUrl);
|
|
});
|
|
|
|
// Remove push notifications (synchronous, no error handling needed)
|
|
PushNotifications.removeServerNotifications(serverUrl);
|
|
|
|
// Invalidate clients (synchronous, no error handling needed)
|
|
NetworkManager.invalidateClient(serverUrl);
|
|
WebsocketManager.invalidateClient(serverUrl);
|
|
|
|
// Remove push disabled acknowledgment (non-critical)
|
|
if (removeServer) {
|
|
await safeExecute('removePushDisabledInServerAcknowledged', async () => {
|
|
const result = await removePushDisabledInServerAcknowledged(urlSafeBase64Encode(serverUrl));
|
|
if (result && typeof result === 'object' && 'error' in result) {
|
|
throw result.error;
|
|
}
|
|
}, false);
|
|
}
|
|
|
|
// Database operations (critical)
|
|
await safeExecute('databaseOperation', async () => {
|
|
if (removeServer) {
|
|
await DatabaseManager.destroyServerDatabase(serverUrl);
|
|
} else {
|
|
await DatabaseManager.deleteServerDatabase(serverUrl);
|
|
}
|
|
});
|
|
|
|
// Reset locale (non-critical)
|
|
await safeExecute('resetLocale', async () => {
|
|
await resetLocale();
|
|
}, false);
|
|
|
|
// Clear cookies (synchronous)
|
|
clearCookiesForServer(serverUrl);
|
|
|
|
// Delete file caches (critical - we need to wipe local data)
|
|
await safeExecute('deleteFileCache', async () => {
|
|
await deleteFileCache(serverUrl);
|
|
});
|
|
|
|
await safeExecute('deleteFileCacheMmPasteInput', async () => {
|
|
await deleteFileCacheByDir('mmPasteInput');
|
|
});
|
|
|
|
await safeExecute('deleteFileCacheThumbnails', async () => {
|
|
await deleteFileCacheByDir('thumbnails');
|
|
});
|
|
|
|
if (errors.length > 0) {
|
|
return {error: errors};
|
|
}
|
|
|
|
return {};
|
|
};
|