[MM-54465] Add Permissions Manager, require manual interaction to approve use of microphone and camera, to send notifications, and to use location per server (#2832) (#2839)

(cherry picked from commit b7e5c6091d)

Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
This commit is contained in:
Mattermost Build 2023-09-15 17:54:42 +03:00 committed by GitHub
parent 92b94deb52
commit 658a8ca7d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 411 additions and 123 deletions

View file

@ -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",
@ -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",

View file

@ -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';
@ -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(),
}));
@ -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(),
@ -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);
});
});
});

View file

@ -32,7 +32,6 @@ import {
TOGGLE_SECURE_INPUT,
} 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';
@ -47,12 +46,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';
@ -392,56 +391,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();

View file

@ -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');
@ -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);

View file

@ -13,6 +13,7 @@ import {PLAY_SOUND} from 'common/communication';
import Config from 'common/config';
import {localizeMessage} from 'main/i18nManager';
import PermissionsManager from 'main/permissionsManager';
import MainWindow from 'main/windows/mainWindow';
import ViewManager from 'main/views/viewManager';
@ -79,6 +80,7 @@ jest.mock('../views/viewManager', () => ({
view: {
server: {
name: 'server_name',
url: new URL('http://someurl.com'),
},
},
}),
@ -93,6 +95,9 @@ jest.mock('../windows/mainWindow', () => ({
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/permissionsManager', () => ({
doPermissionRequest: jest.fn(),
}));
jest.mock('common/config', () => ({}));
@ -103,6 +108,7 @@ describe('main/notifications', () => {
};
beforeEach(() => {
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true));
Notification.isSupported.mockImplementation(() => true);
getFocusAssist.mockReturnValue({value: false});
getDarwinDoNotDisturb.mockReturnValue(false);
@ -115,9 +121,9 @@ describe('main/notifications', () => {
Config.notifications = {};
});
it('should do nothing when Notification is not supported', () => {
it('should do nothing when Notification is not supported', async () => {
Notification.isSupported.mockImplementation(() => false);
displayMention(
await displayMention(
'test',
'test body',
{id: 'channel_id'},
@ -130,14 +136,14 @@ describe('main/notifications', () => {
expect(Notification.didConstruct).not.toBeCalled();
});
it('should do nothing when alarms only is enabled on windows', () => {
it('should do nothing when alarms only is enabled on windows', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'win32',
});
getFocusAssist.mockReturnValue({value: 2});
displayMention(
await displayMention(
'test',
'test body',
{id: 'channel_id'},
@ -154,14 +160,14 @@ describe('main/notifications', () => {
});
});
it('should do nothing when dnd is enabled on mac', () => {
it('should do nothing when dnd is enabled on mac', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',
});
getDarwinDoNotDisturb.mockReturnValue(true);
displayMention(
await displayMention(
'test',
'test body',
{id: 'channel_id'},
@ -178,8 +184,23 @@ describe('main/notifications', () => {
});
});
it('should play notification sound when custom sound is provided', () => {
displayMention(
it('should do nothing when the permission check fails', async () => {
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(false));
await displayMention(
'test',
'test body',
{id: 'channel_id'},
'team_id',
'http://server-1.com/team_id/channel_id',
false,
{id: 1},
{},
);
expect(Notification.didConstruct).not.toBeCalled();
});
it('should play notification sound when custom sound is provided', async () => {
await displayMention(
'test',
'test body',
{id: 'channel_id'},
@ -192,13 +213,13 @@ describe('main/notifications', () => {
expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound');
});
it('should remove existing notification from the same channel/team on windows', () => {
it('should remove existing notification from the same channel/team on windows', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'win32',
});
displayMention(
await displayMention(
'test',
'test body',
{id: 'channel_id'},
@ -213,7 +234,7 @@ describe('main/notifications', () => {
const existingMention = currentNotifications.get('team_id:channel_id');
currentNotifications.delete = jest.fn();
displayMention(
await displayMention(
'test',
'test body 2',
{id: 'channel_id'},
@ -232,8 +253,8 @@ describe('main/notifications', () => {
});
});
it('should switch view when clicking on notification', () => {
displayMention(
it('should switch view when clicking on notification', async () => {
await displayMention(
'click_test',
'mention_click_body',
{id: 'channel_id'},
@ -249,12 +270,12 @@ describe('main/notifications', () => {
expect(ViewManager.showById).toHaveBeenCalledWith('server_id');
});
it('linux/windows - should not flash frame when config item is not set', () => {
it('linux/windows - should not flash frame when config item is not set', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'linux',
});
displayMention(
await displayMention(
'click_test',
'mention_click_body',
{id: 'channel_id'},
@ -270,7 +291,7 @@ describe('main/notifications', () => {
expect(mainWindow.flashFrame).not.toBeCalled();
});
it('linux/windows - should flash frame when config item is set', () => {
it('linux/windows - should flash frame when config item is set', async () => {
Config.notifications = {
flashWindow: true,
};
@ -278,7 +299,7 @@ describe('main/notifications', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
});
displayMention(
await displayMention(
'click_test',
'mention_click_body',
{id: 'channel_id'},
@ -294,12 +315,12 @@ describe('main/notifications', () => {
expect(mainWindow.flashFrame).toBeCalledWith(true);
});
it('mac - should not bounce icon when config item is not set', () => {
it('mac - should not bounce icon when config item is not set', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',
});
displayMention(
await displayMention(
'click_test',
'mention_click_body',
{id: 'channel_id'},
@ -315,7 +336,7 @@ describe('main/notifications', () => {
expect(app.dock.bounce).not.toBeCalled();
});
it('mac - should bounce icon when config item is set', () => {
it('mac - should bounce icon when config item is set', async () => {
Config.notifications = {
bounceIcon: true,
bounceIconType: 'critical',
@ -324,7 +345,7 @@ describe('main/notifications', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
});
displayMention(
await displayMention(
'click_test',
'mention_click_body',
{id: 'channel_id'},

View file

@ -11,6 +11,7 @@ import Config from 'common/config';
import {PLAY_SOUND} from 'common/communication';
import {Logger} from 'common/log';
import PermissionsManager from '../permissionsManager';
import ViewManager from '../views/viewManager';
import MainWindow from '../windows/mainWindow';
@ -24,7 +25,7 @@ export const currentNotifications = new Map();
const log = new Logger('Notifications');
export function displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
export async function displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
log.debug('displayMention', {title, body, channel, teamId, url, silent, data});
if (!Notification.isSupported()) {
@ -49,6 +50,10 @@ export function displayMention(title: string, body: string, channel: {id: string
data,
};
if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) {
return;
}
const mention = new Mention(options, channel, teamId);
const mentionKey = `${mention.teamId}:${mention.channel.id}`;

View file

@ -0,0 +1,178 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {dialog} from 'electron';
import {parseURL, isTrustedURL} from 'common/utils/url';
import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import MainWindow from 'main/windows/mainWindow';
import {PermissionsManager} from './permissionsManager';
jest.mock('fs', () => ({
readFileSync: jest.fn(),
writeFile: jest.fn(),
}));
jest.mock('electron', () => ({
app: {
name: 'Mattermost',
},
ipcMain: {
on: jest.fn(),
},
dialog: {
showMessageBox: jest.fn(),
},
}));
jest.mock('common/utils/url', () => ({
parseURL: jest.fn(),
isTrustedURL: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/views/viewManager', () => ({
getViewByWebContentsId: jest.fn(),
}));
jest.mock('main/windows/callsWidgetWindow', () => ({
isCallsWidget: jest.fn(),
getViewURL: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
describe('main/PermissionsManager', () => {
beforeEach(() => {
MainWindow.get.mockReturnValue({webContents: {id: 1}});
ViewManager.getViewByWebContentsId.mockImplementation((id) => {
if (id === 2) {
return {view: {server: {url: new URL('http://anyurl.com')}}};
}
return null;
});
CallsWidgetWindow.isCallsWidget.mockImplementation((id) => id === 3);
parseURL.mockImplementation((url) => {
try {
return new URL(url);
} catch {
return null;
}
});
isTrustedURL.mockImplementation((url, baseURL) => baseURL.toString().startsWith(url.toString()));
});
afterEach(() => {
jest.resetAllMocks();
});
it('should deny if the permission is not supported', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({}, 'some-other-permission', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(false);
});
it('should allow if the request came from the main window', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 1}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(true);
});
it('should deny if the URL is malformed', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'abadurl!?'});
expect(cb).toHaveBeenCalledWith(false);
});
it('should deny if the server URL can not be found', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 4}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(false);
});
it('should deny if the URL is not trusted', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://wrongurl.com'});
expect(cb).toHaveBeenCalledWith(false);
});
it('should allow if dialog is not required', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 2}, 'fullscreen', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(true);
});
it('should allow if already confirmed by user', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
permissionsManager.json = {
'http://anyurl.com': {
media: {
allowed: true,
},
},
};
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(true);
});
it('should deny if set to permanently deny', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
permissionsManager.json = {
'http://anyurl.com': {
media: {
alwaysDeny: true,
},
},
};
const cb = jest.fn();
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(cb).toHaveBeenCalledWith(false);
});
it('should pop dialog and allow if the user allows, should save to file', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
permissionsManager.writeToFile = jest.fn();
const cb = jest.fn();
dialog.showMessageBox.mockReturnValue(Promise.resolve({response: 0}));
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(permissionsManager.json['http://anyurl.com'].media.allowed).toBe(true);
expect(permissionsManager.writeToFile).toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(true);
});
it('should pop dialog and deny if the user denies', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
permissionsManager.writeToFile = jest.fn();
const cb = jest.fn();
dialog.showMessageBox.mockReturnValue(Promise.resolve({response: 1}));
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(permissionsManager.json['http://anyurl.com'].media.allowed).toBe(false);
expect(permissionsManager.writeToFile).toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(false);
});
it('should pop dialog and deny permanently if the user chooses', async () => {
const permissionsManager = new PermissionsManager('anyfile.json');
permissionsManager.writeToFile = jest.fn();
const cb = jest.fn();
dialog.showMessageBox.mockReturnValue(Promise.resolve({response: 2}));
await permissionsManager.handlePermissionRequest({id: 2}, 'media', cb, {securityOrigin: 'http://anyurl.com'});
expect(permissionsManager.json['http://anyurl.com'].media.allowed).toBe(false);
expect(permissionsManager.json['http://anyurl.com'].media.alwaysDeny).toBe(true);
expect(permissionsManager.writeToFile).toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(false);
});
});

View file

@ -0,0 +1,170 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
PermissionRequestHandlerHandlerDetails,
WebContents,
app,
dialog,
ipcMain,
} from 'electron';
import {UPDATE_PATHS} from 'common/communication';
import JsonFileManager from 'common/JsonFileManager';
import {Logger} from 'common/log';
import {t} from 'common/utils/util';
import {isTrustedURL, parseURL} from 'common/utils/url';
import {permissionsJson} from 'main/constants';
import {localizeMessage} from 'main/i18nManager';
import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import MainWindow from 'main/windows/mainWindow';
const log = new Logger('PermissionsManager');
// supported permission types
const supportedPermissionTypes = [
'media',
'geolocation',
'notifications',
'fullscreen',
'openExternal',
'clipboard-sanitized-write',
];
// permissions that require a dialog
const authorizablePermissionTypes = [
'media',
'geolocation',
'notifications',
];
type Permissions = {
[origin: string]: {
[permission: string]: {
allowed: boolean;
alwaysDeny?: boolean;
};
};
};
export class PermissionsManager extends JsonFileManager<Permissions> {
handlePermissionRequest = async (
webContents: WebContents,
permission: string,
callback: (granted: boolean) => void,
details: PermissionRequestHandlerHandlerDetails,
) => {
callback(await this.doPermissionRequest(
webContents.id,
permission,
details.securityOrigin ?? details.requestingUrl,
));
}
doPermissionRequest = async (
webContentsId: number,
permission: string,
requestingURL: string,
) => {
log.debug('doPermissionRequest', requestingURL, permission);
// is the requested permission type supported?
if (!supportedPermissionTypes.includes(permission)) {
return false;
}
// allow if the request is coming from the local renderer process instead of the remote one
const mainWindow = MainWindow.get();
if (mainWindow && webContentsId === mainWindow.webContents.id) {
return true;
}
const parsedURL = parseURL(requestingURL);
if (!parsedURL) {
return false;
}
let serverURL;
if (CallsWidgetWindow.isCallsWidget(webContentsId)) {
serverURL = CallsWidgetWindow.getViewURL();
} else {
serverURL = ViewManager.getViewByWebContentsId(webContentsId)?.view.server.url;
}
if (!serverURL) {
return false;
}
// is the requesting url trusted?
if (!isTrustedURL(parsedURL, serverURL)) {
return false;
}
// For certain permission types, we need to confirm with the user
if (authorizablePermissionTypes.includes(permission)) {
const currentPermission = this.json[parsedURL.origin]?.[permission];
// If previously allowed, just allow
if (currentPermission?.allowed) {
return true;
}
// If denied permanently, deny
if (currentPermission?.alwaysDeny) {
return false;
}
if (!mainWindow) {
return false;
}
// Show the dialog to ask the user
const {response} = await dialog.showMessageBox(mainWindow, {
title: localizeMessage('main.permissionsManager.checkPermission.dialog.title', 'Permission Requested'),
message: localizeMessage(`main.permissionsManager.checkPermission.dialog.message.${permission}`, '{appName} ({url}) is requesting the "{permission}" permission.', {appName: app.name, url: parsedURL.origin, permission}),
detail: localizeMessage(`main.permissionsManager.checkPermission.dialog.detail.${permission}`, 'Would you like to grant {appName} this permission?', {appName: app.name}),
type: 'question',
buttons: [
localizeMessage('label.allow', 'Allow'),
localizeMessage('label.deny', 'Deny'),
localizeMessage('label.denyPermanently', 'Deny Permanently'),
],
});
// Save their response
const newPermission = {
allowed: response === 0,
alwaysDeny: (response === 2) ? true : undefined,
};
this.json[parsedURL.origin] = {
...this.json[parsedURL.origin],
[permission]: newPermission,
};
this.writeToFile();
if (response > 0) {
return false;
}
}
// We've checked everything so we're okay to grant the remaining cases
return true;
}
}
t('main.permissionsManager.checkPermission.dialog.message.media');
t('main.permissionsManager.checkPermission.dialog.message.geolocation');
t('main.permissionsManager.checkPermission.dialog.message.notifications');
t('main.permissionsManager.checkPermission.dialog.detail.media');
t('main.permissionsManager.checkPermission.dialog.detail.geolocation');
t('main.permissionsManager.checkPermission.dialog.detail.notifications');
let permissionsManager = new PermissionsManager(permissionsJson);
ipcMain.on(UPDATE_PATHS, () => {
permissionsManager = new PermissionsManager(permissionsJson);
});
export default permissionsManager;