1use serde::{Deserialize, Serialize};
45use std::fmt;
46use std::collections::HashSet;
47use std::str::FromStr;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
52pub enum MiniAppPermission {
53 Microphone,
55 Camera,
57 Geolocation,
59 ClipboardRead,
61 ClipboardWrite,
63 Fullscreen,
65 Autoplay,
67 DisplayCapture,
69 Midi,
71 PictureInPicture,
73 ScreenWakeLock,
75 SpeakerSelection,
77 Accelerometer,
79 Gyroscope,
81 Magnetometer,
83 AmbientLightSensor,
85 Bluetooth,
87}
88
89impl MiniAppPermission {
90 pub fn all() -> Vec<MiniAppPermission> {
92 vec![
93 MiniAppPermission::Microphone,
94 MiniAppPermission::Camera,
95 MiniAppPermission::Geolocation,
96 MiniAppPermission::ClipboardRead,
97 MiniAppPermission::ClipboardWrite,
98 MiniAppPermission::Fullscreen,
99 MiniAppPermission::Autoplay,
100 MiniAppPermission::DisplayCapture,
101 MiniAppPermission::Midi,
102 MiniAppPermission::PictureInPicture,
103 MiniAppPermission::ScreenWakeLock,
104 MiniAppPermission::SpeakerSelection,
105 MiniAppPermission::Accelerometer,
106 MiniAppPermission::Gyroscope,
107 MiniAppPermission::Magnetometer,
108 MiniAppPermission::AmbientLightSensor,
109 MiniAppPermission::Bluetooth,
110 ]
111 }
112
113 pub fn policy_name(&self) -> &'static str {
115 match self {
116 MiniAppPermission::Microphone => "microphone",
117 MiniAppPermission::Camera => "camera",
118 MiniAppPermission::Geolocation => "geolocation",
119 MiniAppPermission::ClipboardRead => "clipboard-read",
120 MiniAppPermission::ClipboardWrite => "clipboard-write",
121 MiniAppPermission::Fullscreen => "fullscreen",
122 MiniAppPermission::Autoplay => "autoplay",
123 MiniAppPermission::DisplayCapture => "display-capture",
124 MiniAppPermission::Midi => "midi",
125 MiniAppPermission::PictureInPicture => "picture-in-picture",
126 MiniAppPermission::ScreenWakeLock => "screen-wake-lock",
127 MiniAppPermission::SpeakerSelection => "speaker-selection",
128 MiniAppPermission::Accelerometer => "accelerometer",
129 MiniAppPermission::Gyroscope => "gyroscope",
130 MiniAppPermission::Magnetometer => "magnetometer",
131 MiniAppPermission::AmbientLightSensor => "ambient-light-sensor",
132 MiniAppPermission::Bluetooth => "bluetooth",
133 }
134 }
135
136 pub fn label(&self) -> &'static str {
138 match self {
139 MiniAppPermission::Microphone => "Microphone",
140 MiniAppPermission::Camera => "Camera",
141 MiniAppPermission::Geolocation => "Location",
142 MiniAppPermission::ClipboardRead => "Read Clipboard",
143 MiniAppPermission::ClipboardWrite => "Write Clipboard",
144 MiniAppPermission::Fullscreen => "Fullscreen",
145 MiniAppPermission::Autoplay => "Autoplay Media",
146 MiniAppPermission::DisplayCapture => "Screen Capture",
147 MiniAppPermission::Midi => "MIDI Devices",
148 MiniAppPermission::PictureInPicture => "Picture-in-Picture",
149 MiniAppPermission::ScreenWakeLock => "Keep Screen On",
150 MiniAppPermission::SpeakerSelection => "Speaker Selection",
151 MiniAppPermission::Accelerometer => "Accelerometer",
152 MiniAppPermission::Gyroscope => "Gyroscope",
153 MiniAppPermission::Magnetometer => "Magnetometer",
154 MiniAppPermission::AmbientLightSensor => "Light Sensor",
155 MiniAppPermission::Bluetooth => "Bluetooth",
156 }
157 }
158
159 pub fn description(&self) -> &'static str {
161 match self {
162 MiniAppPermission::Microphone => "Access your microphone for voice chat or recording",
163 MiniAppPermission::Camera => "Access your camera for video calls or photos",
164 MiniAppPermission::Geolocation => "Access your device location",
165 MiniAppPermission::ClipboardRead => "Read text from your clipboard",
166 MiniAppPermission::ClipboardWrite => "Copy text to your clipboard",
167 MiniAppPermission::Fullscreen => "Enter fullscreen mode",
168 MiniAppPermission::Autoplay => "Automatically play audio and video",
169 MiniAppPermission::DisplayCapture => "Capture your screen or window",
170 MiniAppPermission::Midi => "Connect to MIDI instruments and controllers",
171 MiniAppPermission::PictureInPicture => "Play video in a floating window",
172 MiniAppPermission::ScreenWakeLock => "Prevent your screen from sleeping",
173 MiniAppPermission::SpeakerSelection => "Choose which speaker to use for audio",
174 MiniAppPermission::Accelerometer => "Detect device acceleration and movement",
175 MiniAppPermission::Gyroscope => "Detect device rotation and orientation",
176 MiniAppPermission::Magnetometer => "Access compass and magnetic field data",
177 MiniAppPermission::AmbientLightSensor => "Detect ambient light levels",
178 MiniAppPermission::Bluetooth => "Connect to Bluetooth devices",
179 }
180 }
181
182 pub fn icon(&self) -> &'static str {
184 match self {
185 MiniAppPermission::Microphone => "microphone",
186 MiniAppPermission::Camera => "camera",
187 MiniAppPermission::Geolocation => "location",
188 MiniAppPermission::ClipboardRead => "clipboard",
189 MiniAppPermission::ClipboardWrite => "clipboard",
190 MiniAppPermission::Fullscreen => "maximize",
191 MiniAppPermission::Autoplay => "play",
192 MiniAppPermission::DisplayCapture => "monitor",
193 MiniAppPermission::Midi => "music",
194 MiniAppPermission::PictureInPicture => "picture-in-picture",
195 MiniAppPermission::ScreenWakeLock => "sun",
196 MiniAppPermission::SpeakerSelection => "speaker",
197 MiniAppPermission::Accelerometer => "activity",
198 MiniAppPermission::Gyroscope => "rotate-3d",
199 MiniAppPermission::Magnetometer => "compass",
200 MiniAppPermission::AmbientLightSensor => "lightbulb",
201 MiniAppPermission::Bluetooth => "bluetooth",
202 }
203 }
204}
205
206impl fmt::Display for MiniAppPermission {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 write!(f, "{}", self.policy_name())
209 }
210}
211
212impl FromStr for MiniAppPermission {
213 type Err = String;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 match s.to_lowercase().trim() {
217 "microphone" => Ok(MiniAppPermission::Microphone),
218 "camera" => Ok(MiniAppPermission::Camera),
219 "geolocation" | "location" => Ok(MiniAppPermission::Geolocation),
220 "clipboard-read" | "clipboardread" => Ok(MiniAppPermission::ClipboardRead),
221 "clipboard-write" | "clipboardwrite" => Ok(MiniAppPermission::ClipboardWrite),
222 "fullscreen" => Ok(MiniAppPermission::Fullscreen),
223 "autoplay" => Ok(MiniAppPermission::Autoplay),
224 "display-capture" | "displaycapture" => Ok(MiniAppPermission::DisplayCapture),
225 "midi" => Ok(MiniAppPermission::Midi),
226 "picture-in-picture" | "pictureinpicture" | "pip" => Ok(MiniAppPermission::PictureInPicture),
227 "screen-wake-lock" | "screenwakelock" => Ok(MiniAppPermission::ScreenWakeLock),
228 "speaker-selection" | "speakerselection" => Ok(MiniAppPermission::SpeakerSelection),
229 "accelerometer" => Ok(MiniAppPermission::Accelerometer),
230 "gyroscope" => Ok(MiniAppPermission::Gyroscope),
231 "magnetometer" => Ok(MiniAppPermission::Magnetometer),
232 "ambient-light-sensor" | "ambientlightsensor" => Ok(MiniAppPermission::AmbientLightSensor),
233 "bluetooth" => Ok(MiniAppPermission::Bluetooth),
234 other => Err(format!("Unknown permission: {}", other)),
235 }
236 }
237}
238
239pub fn parse_permissions(permissions_str: &str) -> HashSet<MiniAppPermission> {
241 permissions_str
242 .split(',')
243 .filter_map(|s| s.trim().parse().ok())
244 .collect()
245}
246
247#[cfg(test)]
249pub fn serialize_permissions(permissions: &HashSet<MiniAppPermission>) -> String {
250 let mut perms: Vec<_> = permissions.iter().map(|p| p.to_string()).collect();
251 perms.sort();
252 perms.join(",")
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct PermissionInfo {
258 pub id: String,
259 pub label: String,
260 pub description: String,
261 pub icon: String,
262}
263
264impl From<MiniAppPermission> for PermissionInfo {
265 fn from(perm: MiniAppPermission) -> Self {
266 PermissionInfo {
267 id: perm.to_string(),
268 label: perm.label().to_string(),
269 description: perm.description().to_string(),
270 icon: perm.icon().to_string(),
271 }
272 }
273}
274
275pub fn get_all_permission_info() -> Vec<PermissionInfo> {
277 MiniAppPermission::all().into_iter().map(|p| p.into()).collect()
278}
279
280const PERMISSIONS_POLICY_DENY_ALL: &str = concat!(
289 "accelerometer=(), ",
290 "ambient-light-sensor=(), ",
291 "attribution-reporting=(), ",
292 "autoplay=(self), ",
293 "battery=(), ",
294 "bluetooth=(), ",
295 "camera=(), ",
296 "ch-ua=(), ",
297 "ch-ua-arch=(), ",
298 "ch-ua-bitness=(), ",
299 "ch-ua-full-version=(), ",
300 "ch-ua-full-version-list=(), ",
301 "ch-ua-high-entropy-values=(), ",
302 "ch-ua-mobile=(), ",
303 "ch-ua-model=(), ",
304 "ch-ua-platform=(), ",
305 "ch-ua-platform-version=(), ",
306 "ch-ua-wow64=(), ",
307 "compute-pressure=(), ",
308 "cross-origin-isolated=(), ",
309 "direct-sockets=(), ",
310 "display-capture=(), ",
311 "encrypted-media=(), ",
312 "execution-while-not-rendered=(), ",
313 "execution-while-out-of-viewport=(), ",
314 "fullscreen=(), ",
315 "geolocation=(), ",
316 "gyroscope=(), ",
317 "hid=(), ",
318 "identity-credentials-get=(), ",
319 "idle-detection=(), ",
320 "keyboard-map=(), ",
321 "magnetometer=(), ",
322 "mediasession=(), ",
323 "microphone=(), ",
324 "midi=(), ",
325 "navigation-override=(), ",
326 "otp-credentials=(), ",
327 "payment=(), ",
328 "picture-in-picture=(), ",
329 "publickey-credentials-create=(), ",
330 "publickey-credentials-get=(), ",
331 "screen-wake-lock=(), ",
332 "serial=(), ",
333 "sync-xhr=(), ",
334 "storage-access=(), ",
335 "usb=(), ",
336 "web-share=(), ",
337 "window-management=(), ",
338 "xr-spatial-tracking=(), ",
339 "autofill=(), ",
340 "clipboard-read=(), ",
341 "clipboard-write=(), ",
342 "deferred-fetch=(), ",
343 "gamepad=(self), ",
344 "language-detector=(), ",
345 "language-model=(), ",
346 "manual-text=(), ",
347 "rewriter=(), ",
348 "speaker-selection=(), ",
349 "summarizer=(), ",
350 "translator=(), ",
351 "writer=(), ",
352 "all-screens-capture=(), ",
353 "browsing-topics=(), ",
354 "captured-surface-control=(), ",
355 "conversion-measurement=(), ",
356 "digital-credentials-get=(), ",
357 "digital-credentials-create=(), ",
358 "focus-without-user-activation=(), ",
359 "join-ad-interest-group=(), ",
360 "local-fonts=(), ",
361 "monetization=(), ",
362 "run-ad-auction=(), ",
363 "smart-card=(), ",
364 "sync-script=(), ",
365 "trust-token-redemption=(), ",
366 "unload=(), ",
367 "vertical-scroll=(), ",
368 "document-domain=(), ",
369 "window-placement=()",
370);
371
372const GRANTABLE_PERMISSIONS: &[(&str, &str)] = &[
375 ("microphone", "microphone"),
376 ("camera", "camera"),
377 ("geolocation", "geolocation"),
378 ("clipboard-read", "clipboard-read"),
379 ("clipboard-write", "clipboard-write"),
380 ("fullscreen", "fullscreen"),
381 ("autoplay", "autoplay"),
382 ("display-capture", "display-capture"),
383 ("midi", "midi"),
384 ("picture-in-picture", "picture-in-picture"),
385 ("screen-wake-lock", "screen-wake-lock"),
386 ("speaker-selection", "speaker-selection"),
387 ("accelerometer", "accelerometer"),
388 ("gyroscope", "gyroscope"),
389 ("magnetometer", "magnetometer"),
390 ("ambient-light-sensor", "ambient-light-sensor"),
391 ("bluetooth", "bluetooth"),
392];
393
394pub fn build_permissions_policy(granted_permissions: &str) -> String {
405 let granted: std::collections::HashSet<&str> = granted_permissions
407 .split(',')
408 .map(|s| s.trim())
409 .filter(|s| !s.is_empty())
410 .collect();
411
412 if granted.is_empty() {
414 return PERMISSIONS_POLICY_DENY_ALL.to_string();
415 }
416
417 let mut policy = PERMISSIONS_POLICY_DENY_ALL.to_string();
420
421 for (perm_name, directive) in GRANTABLE_PERMISSIONS {
422 if granted.contains(*perm_name) {
423 let deny_pattern = format!("{}=()", directive);
425 let allow_pattern = format!("{}=(self)", directive);
426 policy = policy.replace(&deny_pattern, &allow_pattern);
427 }
428 }
429
430 policy
431}
432
433pub fn build_isolated_permissions_policy(granted_permissions: &str) -> String {
437 let policy = build_permissions_policy(granted_permissions);
438 debug_assert!(policy.contains("cross-origin-isolated=()"));
439 policy.replacen("cross-origin-isolated=()", "cross-origin-isolated=(self)", 1)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn isolated_policy_allows_only_cross_origin_isolation_for_self() {
448 let base = build_permissions_policy("camera");
449 let isolated = build_isolated_permissions_policy("camera");
450 assert!(base.contains("cross-origin-isolated=()"));
451 assert!(isolated.contains("cross-origin-isolated=(self)"));
452 assert!(!isolated.contains("cross-origin-isolated=()"));
453 assert_eq!(base.replace("cross-origin-isolated=()", "cross-origin-isolated=(self)"), isolated);
454 assert!(isolated.contains("camera=(self)") && isolated.contains("microphone=()"));
455 assert!(build_isolated_permissions_policy("").contains("cross-origin-isolated=(self)"));
456 }
457
458 #[test]
459 fn test_parse_permissions() {
460 let perms = parse_permissions("microphone, camera, fullscreen");
461 assert!(perms.contains(&MiniAppPermission::Microphone));
462 assert!(perms.contains(&MiniAppPermission::Camera));
463 assert!(perms.contains(&MiniAppPermission::Fullscreen));
464 assert_eq!(perms.len(), 3);
465 }
466
467 #[test]
468 fn test_serialize_permissions() {
469 let mut perms = HashSet::new();
470 perms.insert(MiniAppPermission::Camera);
471 perms.insert(MiniAppPermission::Microphone);
472 let serialized = serialize_permissions(&perms);
473 assert!(serialized == "camera,microphone" || serialized == "microphone,camera");
475 }
476
477 #[test]
478 fn test_parse_unknown_permission() {
479 let perms = parse_permissions("microphone, unknown, camera");
480 assert_eq!(perms.len(), 2); }
482}