Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,59 @@ yarn start
yarn test
```

### Managed device configuration (MDM)

On company-managed devices, an MDM administrator can force App-lock on, overriding the team-level App-lock
setting. The desktop app reads a single `applockOverride` flag at startup — without spawning any shell
command — and forwards it to the webapp as `window.desktopAppConfig.managedConfig`.

The flag is read from organization-agnostic, Wire-vendor locations (no customer/company name is hardcoded):

| OS | Source | Signal |
| :-- | :-- | :-- |
| Windows | Registry `…\SOFTWARE\Policies\Wire` (HKLM, HKCU fallback) | `applockOverride` value = `1`, **or** the device is MDM-enrolled / Azure-AD joined |
| macOS | App's managed preferences domain (MDM AppConfig profile) | `applockOverride` boolean = `true` |
| Linux | `/etc/wire/managed.json` | `{"applockOverride": true}` (presence means managed unless explicitly `false`) |

#### Testing locally

For each OS: plant the flag, relaunch the app, then open DevTools on the webapp's `<webview>` and evaluate
`window.desktopAppConfig.managedConfig` — expect `{applockOverride: true}`. With nothing planted it must be
`{applockOverride: false}` and the app behaves as before.

**Windows** (elevated prompt):

```cmd
reg add "HKLM\SOFTWARE\Policies\Wire" /v applockOverride /t REG_DWORD /d 1 /f
:: relaunch app -> applockOverride === true
reg delete "HKLM\SOFTWARE\Policies\Wire" /f
```

Enrollment is detected automatically on an Intune/MDM-enrolled or Azure-AD-joined machine (no `Policies\Wire`
value needed).

**macOS** (use the running build's bundle id — production is `com.wearezeta.zclient.mac`; dev/internal differ):

```shell
defaults write com.wearezeta.zclient.mac applockOverride -bool true
# relaunch app -> applockOverride === true
defaults delete com.wearezeta.zclient.mac applockOverride
```

To validate the real MDM path (managed-preferences domain):

```shell
sudo defaults write "/Library/Managed Preferences/com.wearezeta.zclient.mac.plist" applockOverride -bool true
```

**Linux**:

```shell
echo '{"applockOverride": true}' | sudo tee /etc/wire/managed.json
# relaunch app -> applockOverride === true
sudo rm /etc/wire/managed.json
```

### Deployment

| Stage | Branch | Action | Version |
Expand Down
3 changes: 3 additions & 0 deletions electron/src/lib/eventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export const EVENT_TYPE = {
SIGN_OUT: 'EVENT_TYPE.LIFECYCLE.SIGN_OUT',
UNREAD_COUNT: 'EVENT_TYPE.LIFECYCLE.UNREAD_COUNT',
},
MANAGED: {
GET_CONFIG: 'EVENT_TYPE.MANAGED.GET_CONFIG',
},
PREFERENCES: {
SHOW: 'EVENT_TYPE.PREFERENCES.SHOW',
},
Expand Down
7 changes: 7 additions & 0 deletions electron/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {showErrorDialog} from './lib/showDialog';
import * as locale from './locale';
import {ENABLE_LOGGING, getLogger} from './logging/getLogger';
import {getLogFilenames} from './logging/loggerUtils';
import {getManagedConfig} from './managed/ManagedConfig';
import {developerMenu, openDevTools} from './menu/developer';
import * as systemMenu from './menu/system';
import {TrayHandler} from './menu/TrayHandler';
Expand Down Expand Up @@ -198,6 +199,12 @@ const bindIpcEvents = (): void => {
ipcMain.on(EVENT_TYPE.WRAPPER.RELAUNCH, () => lifecycle.relaunch());
ipcMain.on(EVENT_TYPE.ABOUT.SHOW, () => AboutWindow.showWindow());

// Answered synchronously: the webview preload reads this via `ipcRenderer.sendSync` while it builds
// `window.desktopAppConfig`. The value is pre-read and memoized, so the handler does no I/O here.
ipcMain.on(EVENT_TYPE.MANAGED.GET_CONFIG, event => {
event.returnValue = getManagedConfig();
});

ipcMain.handle(EVENT_TYPE.ACTION.GET_OG_DATA, (_event, url) => getOpenGraphDataAsync(url));

ipcMain.on(EVENT_TYPE.ACTION.CHANGE_DOWNLOAD_LOCATION, (_event, downloadPath?: string) => {
Expand Down
71 changes: 71 additions & 0 deletions electron/src/managed/ManagedConfig.test.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import * as fs from 'fs-extra';
import {fake, replace, restore} from 'sinon';

import * as assert from 'assert';

Check warning on line 23 in electron/src/managed/ManagedConfig.test.main.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:assert` over `assert`.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-desktop&issues=AZ76FWkayDH9J9foAWrG&open=AZ76FWkayDH9J9foAWrG&pullRequest=9675

import {clearManagedConfigCache, getManagedConfig} from './ManagedConfig';

import * as EnvironmentUtil from '../runtime/EnvironmentUtil';

describe('getManagedConfig', () => {
beforeEach(() => {
clearManagedConfigCache();
// Force the Linux code path so the test is deterministic regardless of the host OS.
replace(EnvironmentUtil.platform, 'IS_WINDOWS', false);
replace(EnvironmentUtil.platform, 'IS_MAC_OS', false);
replace(EnvironmentUtil.platform, 'IS_LINUX', true);
});

afterEach(() => {
restore();
clearManagedConfigCache();
});

it('enables the override when a managed config payload is present', () => {
replace(fs, 'readJSONSync', fake.returns({applockOverride: true}) as any);
assert.deepStrictEqual(getManagedConfig(), {applockOverride: true});
});

it('disables the override when no managed config is present', () => {
replace(fs, 'readJSONSync', fake.throws(new Error('ENOENT')) as any);
replace(fs, 'pathExistsSync', fake.returns(false) as any);
assert.deepStrictEqual(getManagedConfig(), {applockOverride: false});
});

it('treats a present payload as enabled unless it explicitly opts out', () => {
replace(fs, 'readJSONSync', fake.returns({applockOverride: false}) as any);
assert.deepStrictEqual(getManagedConfig(), {applockOverride: false});
});

it('memoizes the result and reads the underlying source only once', () => {
const readSource = fake.returns({applockOverride: true});
replace(fs, 'readJSONSync', readSource as any);
getManagedConfig();
getManagedConfig();
assert.strictEqual(readSource.callCount, 1);
});

it('never throws and defaults to disabled on an unrecognized platform', () => {
replace(EnvironmentUtil.platform, 'IS_LINUX', false);
assert.deepStrictEqual(getManagedConfig(), {applockOverride: false});
});
});
73 changes: 73 additions & 0 deletions electron/src/managed/ManagedConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {isDeviceManagedLinux} from './backends/linux';
import {isDeviceManagedMacOS} from './backends/macos';
import {isDeviceManagedWindows} from './backends/windows';

import {getLogger} from '../logging/getLogger';
import * as EnvironmentUtil from '../runtime/EnvironmentUtil';

const logger = getLogger('ManagedConfig');

/**
* The single, OS-independent signal the desktop forwards to the webapp. The webapp owns App-lock
* enforcement; the desktop only reports whether the App-lock override applies.
*/
export interface ManagedConfig {
/** When true, App-lock is forced on, overriding the team-level App-lock setting. */
applockOverride: boolean;
}

let cached: ManagedConfig | undefined;

function detectApplockOverride(): boolean {
if (EnvironmentUtil.platform.IS_WINDOWS) {
return isDeviceManagedWindows();
}
if (EnvironmentUtil.platform.IS_MAC_OS) {
return isDeviceManagedMacOS();
}
if (EnvironmentUtil.platform.IS_LINUX) {
return isDeviceManagedLinux();
}
return false;
}

// Reads the managed-device status once, then memoizes it: MDM config is static for an app session
// (a relaunch is required to pick up a re-push). Never throws — any failure is treated as unmanaged
// so non-managed devices and existing on-prem deployments are unaffected.
export function getManagedConfig(): ManagedConfig {
if (!cached) {
let applockOverride = false;
try {
applockOverride = detectApplockOverride();
} catch (error) {
logger.warn('Failed to read managed device configuration, treating the device as unmanaged:', error);
}
cached = {applockOverride};
logger.info(`Managed device detection: applockOverride=${applockOverride}`);
}
return cached;
}

// Clears the memoized result. Primarily for tests; managed status is otherwise stable per session.
export function clearManagedConfigCache(): void {
cached = undefined;
}
41 changes: 41 additions & 0 deletions electron/src/managed/backends/linux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import * as fs from 'fs-extra';

import {getLogger} from '../../logging/getLogger';
import {APPLOCK_OVERRIDE_KEY, LINUX_MANAGED_CONFIG_PATH} from '../constants';

const logger = getLogger('ManagedConfig/linux');

// Linux has no standard MDM enrollment API, so managed status is driven by the presence of a
// machine-wide managed config file placed by the organization. The file's presence means managed,
// unless it explicitly opts out with `{"applockOverride": false}`. A missing/unreadable file means unmanaged.
export function isDeviceManagedLinux(): boolean {
try {
const managedConfig = fs.readJSONSync(LINUX_MANAGED_CONFIG_PATH);
return managedConfig?.[APPLOCK_OVERRIDE_KEY] !== false;
} catch {
// Missing file is the normal unmanaged path; only log genuine parse errors.
if (fs.pathExistsSync(LINUX_MANAGED_CONFIG_PATH)) {
logger.warn(`Failed to parse ${LINUX_MANAGED_CONFIG_PATH}, treating the device as unmanaged.`);
}
return false;
}
}
38 changes: 38 additions & 0 deletions electron/src/managed/backends/macos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {systemPreferences} from 'electron';

import {getLogger} from '../../logging/getLogger';
import {APPLOCK_OVERRIDE_KEY} from '../constants';

const logger = getLogger('ManagedConfig/macos');

// macOS does not expose MDM enrollment status without a shell tool (`profiles`), which is disallowed,
// and Electron has no enrollment API. So managed status is driven by the MDM-pushed managed app
// preference — the standard MDM AppConfig mechanism. `getUserDefault` reads it natively from the app's
// own defaults domain (no shell), returning `false` when the preference is not set.
export function isDeviceManagedMacOS(): boolean {
try {
return systemPreferences.getUserDefault(APPLOCK_OVERRIDE_KEY, 'boolean') === true;
} catch (error) {
logger.warn('Failed to read managed preference, treating the device as unmanaged:', error);
return false;
}
}
Loading
Loading