Skip to content

Commit

Permalink
[MM-54465] Add Permissions Manager, require manual interaction to app…
Browse files Browse the repository at this point in the history
…rove use of microphone and camera, to send notifications, and to use location per server
  • Loading branch information
devinbinnie committed Sep 13, 2023
1 parent ce14475 commit 07e88b7
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 123 deletions.
10 changes: 10 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
"common.tabs.TAB_PLAYBOOKS": "Playbooks",
"label.accept": "Accept",
"label.add": "Add",
"label.allow": "Allow",
"label.cancel": "Cancel",
"label.change": "Change",
"label.close": "Close",
"label.deny": "Deny",
"label.denyPermanently": "Deny Permanently",
"label.login": "Login",
"label.no": "No",
"label.ok": "OK",
Expand Down Expand Up @@ -105,6 +108,13 @@
"main.notifications.upgrade.newVersion.title": "New desktop version available",
"main.notifications.upgrade.readyToInstall.body": "A new desktop version is ready to install now.",
"main.notifications.upgrade.readyToInstall.title": "Click to restart and install update",
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "{appName} will use the location for setting up your timezone. You can always change this later in your computer's settings.",
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} will use the microphone and camera for calls and voice messages. You can always change this later in your computer's settings.",
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} will send you notifications for messages and calls. You can configure your notification preferences in Settings.",
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) would like to access your location.",
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) would like to access the microphone and camera.",
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) would like to send you notifications.",
"main.permissionsManager.checkPermission.dialog.title": "Permission Requested",
"main.tray.tray.expired": "Session Expired: Please sign in to continue receiving notifications.",
"main.tray.tray.mention": "You have been mentioned",
"main.tray.tray.unread": "You have unread channels",
Expand Down
52 changes: 1 addition & 51 deletions src/main/app/initialize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import path from 'path';
import {app, session} from 'electron';

import Config from 'common/config';
import {parseURL, isTrustedURL} from 'common/utils/url';

import parseArgs from 'main/ParseArgs';
import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow';

import {initialize} from './initialize';
import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils';
Expand Down Expand Up @@ -107,11 +105,6 @@ jest.mock('common/config', () => ({
initRegistry: jest.fn(),
}));

jest.mock('common/utils/url', () => ({
parseURL: jest.fn(),
isTrustedURL: jest.fn(),
}));

jest.mock('main/allowProtocolDialog', () => ({
init: jest.fn(),
}));
Expand Down Expand Up @@ -169,9 +162,7 @@ jest.mock('main/UserActivityMonitor', () => ({
on: jest.fn(),
startMonitoring: jest.fn(),
}));
jest.mock('main/windows/callsWidgetWindow', () => ({
isCallsWidget: jest.fn(),
}));
jest.mock('main/windows/callsWidgetWindow', () => ({}));
jest.mock('main/views/viewManager', () => ({
getViewByWebContentsId: jest.fn(),
handleDeepLink: jest.fn(),
Expand Down Expand Up @@ -277,46 +268,5 @@ describe('main/app/initialize', () => {

expect(ViewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com');
});

it('should allow permission requests for supported types from trusted URLs', async () => {
ViewManager.getViewByWebContentsId.mockReturnValue({
view: {
server: {
url: new URL('http://server-1.com'),
},
},
});
parseURL.mockImplementation((url) => new URL(url));
isTrustedURL.mockImplementation((url) => url.toString() === 'http://server-1.com/');

let callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback);
});
await initialize();
expect(callback).toHaveBeenCalledWith(false);

callback = jest.fn();
MainWindow.get.mockReturnValue({webContents: {id: 1}});
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 1, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
});
await initialize();
expect(callback).toHaveBeenCalledWith(true);

callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 2, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
});
await initialize();
expect(callback).toHaveBeenCalledWith(true);

callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 2, getURL: () => 'http://server-2.com'}, 'openExternal', callback);
});
await initialize();
expect(callback).toHaveBeenCalledWith(false);
});
});
});
52 changes: 2 additions & 50 deletions src/main/app/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
DOUBLE_CLICK_ON_WINDOW,
} from 'common/communication';
import Config from 'common/config';
import {isTrustedURL, parseURL} from 'common/utils/url';
import {Logger} from 'common/log';

import AllowProtocolDialog from 'main/allowProtocolDialog';
Expand All @@ -46,12 +45,12 @@ import CriticalErrorHandler from 'main/CriticalErrorHandler';
import downloadsManager from 'main/downloadsManager';
import i18nManager from 'main/i18nManager';
import parseArgs from 'main/ParseArgs';
import PermissionsManager from 'main/permissionsManager';
import ServerManager from 'common/servers/serverManager';
import TrustedOriginsStore from 'main/trustedOrigins';
import Tray from 'main/tray/tray';
import UserActivityMonitor from 'main/UserActivityMonitor';
import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import MainWindow from 'main/windows/mainWindow';

import {protocols} from '../../../electron-builder.json';
Expand Down Expand Up @@ -388,56 +387,9 @@ async function initializeAfterAppReady() {

ipcMain.emit('update-dict');

// supported permission types
const supportedPermissionTypes = [
'media',
'geolocation',
'notifications',
'fullscreen',
'openExternal',
'clipboard-sanitized-write',
];

// handle permission requests
// - approve if a supported permission type and the request comes from the renderer or one of the defined servers
defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
log.debug('permission requested', webContents.getURL(), permission);

// is the requested permission type supported?
if (!supportedPermissionTypes.includes(permission)) {
callback(false);
return;
}

// is the request coming from the renderer?
const mainWindow = MainWindow.get();
if (mainWindow && webContents.id === mainWindow.webContents.id) {
callback(true);
return;
}

if (CallsWidgetWindow.isCallsWidget(webContents.id)) {
callback(true);
return;
}

const requestingURL = webContents.getURL();
const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.view.server.url;

if (!serverURL) {
callback(false);
return;
}

const parsedURL = parseURL(requestingURL);
if (!parsedURL) {
callback(false);
return;
}

// is the requesting url trusted?
callback(isTrustedURL(parsedURL, serverURL));
});
defaultSession.setPermissionRequestHandler(PermissionsManager.handlePermissionRequest);

if (wasUpdated(AppVersionManager.lastAppVersion)) {
clearAppCache();
Expand Down
2 changes: 2 additions & 0 deletions src/main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export let trustedOriginsStoreFile = '';
export let boundsInfoPath = '';
export let migrationInfoPath = '';
export let downloadsJson = '';
export let permissionsJson = '';

export function updatePaths(emit = false) {
userDataPath = app.getPath('userData');
Expand All @@ -31,6 +32,7 @@ export function updatePaths(emit = false) {
boundsInfoPath = path.join(userDataPath, 'bounds-info.json');
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
downloadsJson = path.resolve(userDataPath, 'downloads.json');
permissionsJson = path.resolve(userDataPath, 'permissions.json');

if (emit) {
ipcMain.emit(UPDATE_PATHS);
Expand Down
Loading

0 comments on commit 07e88b7

Please sign in to comment.