1
>
/*---------------------------------------------------------------------------------------------
browserPermissions.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import { Codicon } from '../../../base/common/codicons.js';
7
>
import { Emitter, Event } from '../../../base/common/event.js';
8
>
import { Disposable } from '../../../base/common/lifecycle.js';
9
>
import { ThemeIcon } from '../../../base/common/themables.js';
10
>
import { localize } from '../../../nls.js';
11
>
12
>
/**
13
>
* UI-agnostic, per-origin permission model for the integrated browser, modeled
14
>
* on Chromium's user-friendly Site Settings categories rather than the raw
15
>
* Electron permission strings. This module intentionally ships NO UI and no
16
>
* Electron import so it can load in both the main process (authoritative store)
17
>
* and the workbench renderer (read mirror hydrated from storage).
18
>
*
19
>
* The Electron permission string list is taken from Electron's own source
20
>
* (`shell/common/gin_converters/content_converter.cc`), the single source of
21
>
* truth for the strings passed to the permission handlers. Any
22
>
* `blink::PermissionType` Electron does not explicitly name is reported as
23
>
* `'unknown'`.
24
>
*/
25
>
26
>
/**
27
>
* A decision a user can record for a (origin, category) pair. These are the
28
>
* only two values ever persisted; clearing a decision removes it entirely.
29
>
* - 'allow' -> grant without prompting
30
>
* - 'deny' -> reject without prompting
31
>
*/
32
>
export type PermissionDecision = 'allow' | 'deny';
33
>
34
>
/**
35
>
* The effective state of a (origin, category) pair: either a recorded
36
>
* {@link PermissionDecision}, or 'ask' when no decision has been recorded.
37
>
* 'ask' is only ever a default/effective value -- it is never stored.
38
>
*/
39
>
export type PermissionState = PermissionDecision | 'ask';
40
>
41
>
/**
42
>
* User-facing permission categories. These are deliberately coarser and more
43
>
* meaningful than Electron's raw permission strings. For example Electron's
44
>
* single `media` permission is split into `Camera` and `Microphone`, and the
45
>
* various clipboard permissions collapse into `Clipboard`.
46
>
*/
47
>
export const enum PermissionCategory {
48
>
Location = 'location',
49
>
Camera = 'camera',
50
>
Microphone = 'microphone',
51
>
Notifications = 'notifications',
52
>
Sensors = 'sensors',
53
>
Clipboard = 'clipboard',
54
>
Devices = 'devices',
55
>
}
56
>
57
>
/**
58
>
* The kinds of hardware-device chooser flows the {@link PermissionCategory.Devices}
59
>
* category gates. Each maps to a distinct Electron device-selection event but is
60
>
* surfaced to the user through one unified request/selection flow.
61
>
*/
62
>
export type BrowserDeviceType = 'usb' | 'serial' | 'hid' | 'bluetooth';
63
>
64
>
/**
65
>
* A single hardware device offered to the user during a device-chooser flow.
66
>
* Only plain, user-presentable data crosses the IPC boundary; the opaque
67
>
* `deviceId` is echoed back verbatim to select the device.
68
>
*/
69
>
export interface IBrowserDeviceCandidate {
70
>
/** Opaque, device-type-specific identifier echoed back to select the device. */
71
>
readonly deviceId: string;
72
>
/** Primary, user-facing label (e.g. product name). */
73
>
readonly label: string;
74
>
/** Optional secondary detail (e.g. manufacturer or vendor:product ids). */
75
>
readonly detail?: string;
76
>
}
77
>
78
>
/**
79
>
* Static metadata describing a category and how it maps to Electron's raw
80
>
* permission strings. A UI can iterate {@link PERMISSION_CATEGORY_DESCRIPTORS}
81
>
* to render a settings list directly from this data.
82
>
*/
83
>
export interface IPermissionCategoryDescriptor {
84
>
readonly category: PermissionCategory;
85
>
/** Short, human-readable label suitable for a settings row. */
86
>
readonly label: string;
87
>
/** One-line description of what granting this category enables. */
88
>
readonly description: string;
89
>
/** The icon to display for this category. */
90
>
readonly icon: ThemeIcon;
91
>
/** Electron permission strings that map to this category. */
92
>
readonly permissions: string[];
93
>
/** State assumed for this category when an origin has not recorded a decision. */
94
>
readonly defaultState: PermissionState;
95
>
}
96
>
97
>
export const PERMISSION_CATEGORY_DESCRIPTORS: Readonly<Record<PermissionCategory, IPermissionCategoryDescriptor>> = {
98
>
[PermissionCategory.Location]: {
99
>
category: PermissionCategory.Location,
100
>
label: localize('browserPermission.location.label', "Location"),
101
>
description: localize('browserPermission.location.description', "Access this device's geographic location"),
102
>
icon: Codicon.location,
103
>
permissions: ['geolocation', 'geolocation-approximate'],
104
>
defaultState: 'ask',
105
>
},
106
>
[PermissionCategory.Camera]: {
107
>
category: PermissionCategory.Camera,
108
>
label: localize('browserPermission.camera.label', "Camera"),
109
>
description: localize('browserPermission.camera.description', "Capture video from cameras"),
110
>
icon: Codicon.deviceCamera,
111
>
// `media` is shared with Microphone; disambiguated via mediaType/mediaTypes.
112
>
permissions: ['media'],
113
>
defaultState: 'ask',
114
>
},
115
>
[PermissionCategory.Microphone]: {
116
>
category: PermissionCategory.Microphone,
117
>
label: localize('browserPermission.microphone.label', "Microphone"),
118
>
description: localize('browserPermission.microphone.description', "Capture audio from microphones"),
119
>
icon: Codicon.mic,
120
>
permissions: ['media'],
121
>
defaultState: 'ask',
122
>
},
123
>
[PermissionCategory.Sensors]: {
124
>
category: PermissionCategory.Sensors,
125
>
label: localize('browserPermission.sensors.label', "Sensors"),
126
>
description: localize('browserPermission.sensors.description', "Read motion and environmental sensors"),
127
>
icon: Codicon.pulse,
128
>
permissions: ['sensors'],
129
>
defaultState: 'allow',
130
>
},
131
>
[PermissionCategory.Clipboard]: {
132
>
category: PermissionCategory.Clipboard,
133
>
label: localize('browserPermission.clipboard.label', "Clipboard"),
134
>
description: localize('browserPermission.clipboard.description', "Read from and write to the system clipboard"),
135
>
icon: Codicon.clippy,
136
>
permissions: ['clipboard-read'],
137
>
defaultState: 'ask',
138
>
},
139
>
[PermissionCategory.Notifications]: {
140
>
category: PermissionCategory.Notifications,
141
>
label: localize('browserPermission.notifications.label', "Notifications"),
142
>
description: localize('browserPermission.notifications.description', "Display desktop notifications"),
143
>
icon: Codicon.bell,
144
>
permissions: ['notifications'],
145
>
defaultState: 'ask',
146
>
},
147
>
[PermissionCategory.Devices]: {
148
>
category: PermissionCategory.Devices,
149
>
label: localize('browserPermission.devices.label', "Devices"),
150
>
description: localize('browserPermission.devices.description', "Request access to USB, serial, HID, and Bluetooth devices"),
151
>
icon: Codicon.plug,
152
>
// Each device kind has its own native chooser; this decision only gates
153
>
// whether that chooser is allowed to surface. Bluetooth has no Electron
154
>
// permission string (it is gated in the chooser handler directly).
155
>
permissions: ['usb', 'serial', 'hid'],
156
>
defaultState: 'allow',
157
>
},
158
>
/**
159
>
* Permissions not listed here are either always allowed (see
160
>
* {@link ALWAYS_ALLOWED_PERMISSIONS}) or, by default, always denied:
161
>
*
162
>
* No-op in Electron due to missing backend support
163
>
* - Smart Cards (`smart-card`)
164
>
* - NFC (`nfc`)
165
>
* - Protected Content (`mediaKeySystem`)
166
>
* - Augmented / Virtual Reality, Hand Tracking (`ar`, `vr`, `hand-tracking`)
167
>
* - Payment Handlers (`payment-handler`)
168
>
* - Background Sync (`background-sync`, `periodic-background-sync`, `background-fetch`)
169
>
* - Printing (`web-printing`)
170
>
* - App Installation (`web-app-installation`)
171
>
* - Storage Access (`storage-access`, `top-level-storage-access`)
172
>
*
173
>
* Not currently implemented (in approximate order of 'might want')
174
>
* - Local Network Access (`local-network-access`, `local-network`, `loopback-network`)
175
>
* - Screen Capture, Captured Surface Control (`display-capture`, `captured-surface-control`)
176
>
* - File Writing (`fileSystem`)
177
>
* - Open External (`openExternal`)
178
>
* - MIDI (`midi`, `midiSysex`)
179
>
* - Persistent Storage (`persistent-storage`)
180
>
* - Device Activity (`idle-detection`)
181
>
* - Audio Output (`speaker-selection`)
182
>
* - Wake Lock (`screen-wake-lock`, `system-wake-lock`)
183
>
* - Window Management (`window-management`)
184
>
* - Fonts (`local-fonts`)
185
>
* - Automatic Fullscreen (`automatic-fullscreen`)
186
>
*/
187
>
};
188
>
189
>
/**
190
>
* Raw Electron permission strings that are granted unconditionally, with no
191
>
* recorded state and no management control. These are low-risk capabilities
192
>
* that Chrome itself also always grants automatically.
193
>
*/
194
>
export const ALWAYS_ALLOWED_PERMISSIONS: ReadonlySet<string> = new Set([
195
>
'pointerLock',
196
>
'keyboardLock',
197
>
'fullscreen',
198
>
'clipboard-sanitized-write',
199
>
]);
200
>
201
>
/** Whether a raw Electron permission string is granted unconditionally. */
202
>
export function isAlwaysAllowedPermission(permission: string): boolean {
203
return ALWAYS_ALLOWED_PERMISSIONS.has(permission);
204
}
206
>
/** All categories, in a stable display order. */
207
>
export const ALL_PERMISSION_CATEGORIES: readonly PermissionCategory[] = Object.keys(PERMISSION_CATEGORY_DESCRIPTORS) as PermissionCategory[];
208
>
209
>
/** The default state for each permission category. */
210
>
const DEFAULT_PERMISSION_STATES: Readonly<Record<PermissionCategory, PermissionState>> = Object.freeze(
211
>
Object.fromEntries(ALL_PERMISSION_CATEGORIES.map(category => [category, PERMISSION_CATEGORY_DESCRIPTORS[category].defaultState])) as Record<PermissionCategory, PermissionState>
212
>
);
213
>
214
>
/**
215
>
* Reverse lookup table built once from the descriptors:
216
>
* electron permission string -> categories that own it.
217
>
* `media` is intentionally omitted here because it requires `details` to
218
>
* disambiguate; it is handled explicitly in {@link electronPermissionToCategories}.
219
>
*/
220
>
const PERMISSION_TO_CATEGORIES: ReadonlyMap<string, PermissionCategory[]> = (() => {
221
>
const map = new Map<string, PermissionCategory[]>();
222
>
for (const category of ALL_PERMISSION_CATEGORIES) {
223
>
for (const permission of PERMISSION_CATEGORY_DESCRIPTORS[category].permissions) {
224
>
if (permission === 'media') {
225
>
continue;
226
>
}
227
>
const existing = map.get(permission);
228
>
if (existing) {
229
existing.push(category);
231
>
map.set(permission, [category]);
232
>
}
233
>
}
234
>
}
235
>
return map;
236
>
})();
237
>
238
>
/**
239
>
* Map a raw Electron permission string (from either handler, or a device type)
240
>
* to the user-friendly category/categories it represents.
241
>
*
242
>
* Notes:
243
>
* - `media` resolves to `Camera`, `Microphone`, or both depending on the
244
>
* normalized `mediaKinds` hint extracted by the caller from Electron's
245
>
* details. With no hint it conservatively resolves to both, so the caller
246
>
* can require the strictest decision.
247
>
* - `unknown` (and anything unrecognized) resolves to an empty array.
248
>
*/
249
>
export function electronPermissionToCategories(permission: string, mediaKinds?: ReadonlyArray<'video' | 'audio'>): PermissionCategory[] {
250
if (permission === 'media') {
251
return resolveMediaCategories(mediaKinds);