* 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>
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {Q} from '@nozbe/watermelondb';
|
|
import deepEqual from 'deep-equal';
|
|
import {DeviceEventEmitter} from 'react-native';
|
|
|
|
import {Events} from '@constants';
|
|
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
|
import DatabaseManager from '@database/manager';
|
|
import {getServerCredentials} from '@init/credentials';
|
|
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
|
import {getConfig, getLicense, getGlobalDataRetentionPolicy, getGranularDataRetentionPolicies, getLastGlobalDataRetentionRun, getIsDataRetentionEnabled} from '@queries/servers/system';
|
|
import PostModel from '@typings/database/models/servers/post';
|
|
import {logError} from '@utils/log';
|
|
|
|
import {deletePosts} from './post';
|
|
|
|
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
|
|
|
|
const {SERVER: {POST}} = MM_TABLES;
|
|
|
|
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
|
|
try {
|
|
// If we have credentials for this server then update the values in the database
|
|
const credentials = await getServerCredentials(serverUrl);
|
|
if (credentials) {
|
|
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const currentLicense = await getLicense(database);
|
|
const systems: IdValue[] = [];
|
|
|
|
if (!deepEqual(license, currentLicense)) {
|
|
systems.push({
|
|
id: SYSTEM_IDENTIFIERS.LICENSE,
|
|
value: JSON.stringify(license),
|
|
});
|
|
}
|
|
|
|
if (systems.length) {
|
|
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
|
DeviceEventEmitter.emit(Events.LICENSE_CHANGED, {serverUrl, license});
|
|
}
|
|
|
|
return await storeConfig(serverUrl, config);
|
|
}
|
|
} catch (error) {
|
|
logError('An error occurred while saving config & license', error);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export async function storeConfig(serverUrl: string, config: ClientConfig | undefined, prepareRecordsOnly = false) {
|
|
if (!config) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const currentConfig = await getConfig(database);
|
|
const configsToUpdate: IdValue[] = [];
|
|
const configsToDelete: IdValue[] = [];
|
|
|
|
let k: keyof ClientConfig;
|
|
for (k in config) {
|
|
if (currentConfig?.[k] !== config[k]) {
|
|
configsToUpdate.push({
|
|
id: k,
|
|
value: config[k],
|
|
});
|
|
}
|
|
}
|
|
for (k in currentConfig) {
|
|
if (config[k] === undefined) {
|
|
configsToDelete.push({
|
|
id: k,
|
|
value: currentConfig[k],
|
|
});
|
|
}
|
|
}
|
|
|
|
if (configsToDelete.length || configsToUpdate.length) {
|
|
const results = await operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
|
|
DeviceEventEmitter.emit(Events.CONFIG_CHANGED, {serverUrl, config});
|
|
return results;
|
|
}
|
|
} catch (error) {
|
|
logError('storeConfig', error);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export async function storeDataRetentionPolicies(serverUrl: string, data: DataRetentionPoliciesRequest, prepareRecordsOnly = false) {
|
|
try {
|
|
const {globalPolicy, teamPolicies, channelPolicies} = data;
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const systems: IdValue[] = [{
|
|
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
|
|
value: globalPolicy || {},
|
|
}, {
|
|
id: SYSTEM_IDENTIFIERS.GRANULAR_DATA_RETENTION_POLICIES,
|
|
value: {
|
|
team: teamPolicies || [],
|
|
channel: channelPolicies || [],
|
|
},
|
|
}];
|
|
|
|
return operator.handleSystem({
|
|
systems,
|
|
prepareRecordsOnly,
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function updateLastDataRetentionRun(serverUrl: string, value?: number, prepareRecordsOnly = false) {
|
|
try {
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
|
|
const systems: IdValue[] = [{
|
|
id: SYSTEM_IDENTIFIERS.LAST_DATA_RETENTION_RUN,
|
|
value: value || Date.now(),
|
|
}];
|
|
|
|
return operator.handleSystem({systems, prepareRecordsOnly});
|
|
} catch (error) {
|
|
logError('Failed updateLastDataRetentionRun', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
export async function dataRetentionCleanup(serverUrl: string) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
|
|
const lastRunAt = await getLastGlobalDataRetentionRun(database);
|
|
const lastCleanedToday = new Date(lastRunAt).toDateString() === new Date().toDateString();
|
|
|
|
// Do not run if clean up is already done today
|
|
if (lastRunAt && lastCleanedToday) {
|
|
return {error: undefined};
|
|
}
|
|
|
|
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
|
const result = await (isDataRetentionEnabled ? dataRetentionPolicyCleanup(serverUrl) : dataRetentionWithoutPolicyCleanup(serverUrl));
|
|
|
|
if (!result.error) {
|
|
await updateLastDataRetentionRun(serverUrl);
|
|
}
|
|
|
|
await database.unsafeVacuum();
|
|
|
|
return result;
|
|
} catch (error) {
|
|
logError('An error occurred while performing data retention cleanup', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
async function dataRetentionPolicyCleanup(serverUrl: string) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const globalPolicy = await getGlobalDataRetentionPolicy(database);
|
|
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
|
|
|
|
// Get global data retention cutoff
|
|
let globalRetentionCutoff = 0;
|
|
if (globalPolicy?.message_deletion_enabled) {
|
|
globalRetentionCutoff = globalPolicy.message_retention_cutoff;
|
|
}
|
|
|
|
// Get Granular data retention policies
|
|
let teamPolicies: TeamDataRetentionPolicy[] = [];
|
|
let channelPolicies: ChannelDataRetentionPolicy[] = [];
|
|
if (granularPoliciesData) {
|
|
teamPolicies = granularPoliciesData.team;
|
|
channelPolicies = granularPoliciesData.channel;
|
|
}
|
|
|
|
const channelsCutoffs: {[key: string]: number} = {};
|
|
|
|
// Get channel level cutoff from team policies
|
|
for await (const teamPolicy of teamPolicies) {
|
|
const {team_id, post_duration} = teamPolicy;
|
|
const channelIds = await queryAllChannelsForTeam(database, team_id).fetchIds();
|
|
if (channelIds.length) {
|
|
const cutoff = getDataRetentionPolicyCutoff(post_duration);
|
|
channelIds.forEach((channelId) => {
|
|
channelsCutoffs[channelId] = cutoff;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get channel level cutoff from channel policies
|
|
channelPolicies.forEach(({channel_id, post_duration}) => {
|
|
channelsCutoffs[channel_id] = getDataRetentionPolicyCutoff(post_duration);
|
|
});
|
|
|
|
const conditions = [];
|
|
|
|
const channelIds = Object.keys(channelsCutoffs);
|
|
if (channelIds.length) {
|
|
// Fetch posts by channel level cutoff
|
|
for (const channelId of channelIds) {
|
|
const cutoff = channelsCutoffs[channelId];
|
|
conditions.push(`(channel_id='${channelId}' AND create_at < ${cutoff})`);
|
|
}
|
|
|
|
// Fetch posts by global cutoff which are not already fetched by channel level cutoff
|
|
conditions.push(`(channel_id NOT IN ('${channelIds.join("','")}') AND create_at < ${globalRetentionCutoff})`);
|
|
} else {
|
|
conditions.push(`create_at < ${globalRetentionCutoff}`);
|
|
}
|
|
|
|
const postIds = await database.get<PostModel>(POST).query(
|
|
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
|
|
).fetchIds();
|
|
|
|
return dataRetentionCleanPosts(serverUrl, postIds);
|
|
} catch (error) {
|
|
logError('An error occurred while performing data retention policy cleanup', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
async function dataRetentionWithoutPolicyCleanup(serverUrl: string) {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const cutoff = getDataRetentionPolicyCutoff(14); // 14 days
|
|
|
|
const postIds = await database.get<PostModel>(POST).query(
|
|
Q.where('create_at', Q.lt(cutoff)),
|
|
).fetchIds();
|
|
|
|
return dataRetentionCleanPosts(serverUrl, postIds);
|
|
} catch (error) {
|
|
logError('An error occurred while performing data retention without policy cleanup', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
async function dataRetentionCleanPosts(serverUrl: string, postIds: string[]) {
|
|
if (postIds.length) {
|
|
const batchSize = 1000;
|
|
const deletePromises = [];
|
|
for (let i = 0; i < postIds.length; i += batchSize) {
|
|
const batch = postIds.slice(i, batchSize);
|
|
deletePromises.push(
|
|
deletePosts(serverUrl, batch),
|
|
);
|
|
}
|
|
const deleteResult = await Promise.all(deletePromises);
|
|
for (const {error} of deleteResult) {
|
|
if (error) {
|
|
return {error};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {error: undefined};
|
|
}
|
|
|
|
// Returns cutoff time based on the policy's post_duration
|
|
function getDataRetentionPolicyCutoff(postDuration: number) {
|
|
const periodDate = new Date();
|
|
periodDate.setDate(periodDate.getDate() - postDuration);
|
|
periodDate.setHours(0);
|
|
periodDate.setMinutes(0);
|
|
periodDate.setSeconds(0);
|
|
return periodDate.getTime();
|
|
}
|
|
|
|
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
|
|
try {
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
|
|
value: reset ? 0 : Date.now(),
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
return {error: undefined};
|
|
} catch (error) {
|
|
logError('setLastServerVersionCheck', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
export async function setGlobalThreadsTab(serverUrl: string, globalThreadsTab: GlobalThreadsTab) {
|
|
try {
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.GLOBAL_THREADS_TAB,
|
|
value: globalThreadsTab,
|
|
}],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
return {error: undefined};
|
|
} catch (error) {
|
|
logError('setGlobalThreadsTab', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
|
|
try {
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
|
|
return {error: undefined};
|
|
} catch (error) {
|
|
logError('An error occurred while dismissing an announcement', error);
|
|
return {error};
|
|
}
|
|
}
|