rama_http_headers/common/permissions_policy/
mod.rs1mod directive;
9
10pub use self::directive::{
11 AllowlistSource, PermissionsPolicyDirective, PermissionsPolicyDirectiveName,
12};
13
14use std::fmt;
15
16use rama_http_types::{HeaderName, HeaderValue};
17use rama_utils::macros::generate_set_and_with;
18
19use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
20
21#[derive(Debug, Clone, PartialEq, Eq, Default)]
71pub struct PermissionsPolicy {
72 directives: Vec<PermissionsPolicyDirective>,
73}
74
75impl PermissionsPolicy {
76 #[must_use]
79 pub const fn empty() -> Self {
80 Self {
81 directives: Vec::new(),
82 }
83 }
84
85 pub fn directives(&self) -> impl Iterator<Item = &PermissionsPolicyDirective> + '_ {
87 self.directives.iter()
88 }
89
90 generate_set_and_with! {
91 pub fn directive(mut self, directive: PermissionsPolicyDirective) -> Self {
99 if let Some(slot) = self
100 .directives
101 .iter_mut()
102 .find(|d| d.name == directive.name)
103 {
104 slot.allow_list = directive.allow_list;
105 } else {
106 self.directives.push(directive);
107 }
108 self
109 }
110 }
111
112 generate_set_and_with! {
121 pub fn deny_camera(mut self) -> Self {
123 self.set_directive(PermissionsPolicyDirective::deny(
124 PermissionsPolicyDirectiveName::Camera,
125 ));
126 self
127 }
128 }
129 generate_set_and_with! {
130 pub fn deny_microphone(mut self) -> Self {
132 self.set_directive(PermissionsPolicyDirective::deny(
133 PermissionsPolicyDirectiveName::Microphone,
134 ));
135 self
136 }
137 }
138 generate_set_and_with! {
139 pub fn deny_geolocation(mut self) -> Self {
141 self.set_directive(PermissionsPolicyDirective::deny(
142 PermissionsPolicyDirectiveName::Geolocation,
143 ));
144 self
145 }
146 }
147 generate_set_and_with! {
148 pub fn deny_payment(mut self) -> Self {
150 self.set_directive(PermissionsPolicyDirective::deny(
151 PermissionsPolicyDirectiveName::Payment,
152 ));
153 self
154 }
155 }
156 generate_set_and_with! {
157 pub fn deny_usb(mut self) -> Self {
159 self.set_directive(PermissionsPolicyDirective::deny(
160 PermissionsPolicyDirectiveName::Usb,
161 ));
162 self
163 }
164 }
165 generate_set_and_with! {
166 pub fn deny_interest_cohort(mut self) -> Self {
171 self.set_directive(PermissionsPolicyDirective::deny(
172 PermissionsPolicyDirectiveName::InterestCohort,
173 ));
174 self
175 }
176 }
177 generate_set_and_with! {
178 pub fn deny_browsing_topics(mut self) -> Self {
181 self.set_directive(PermissionsPolicyDirective::deny(
182 PermissionsPolicyDirectiveName::BrowsingTopics,
183 ));
184 self
185 }
186 }
187 generate_set_and_with! {
188 pub fn deny_attribution_reporting(mut self) -> Self {
191 self.set_directive(PermissionsPolicyDirective::deny(
192 PermissionsPolicyDirectiveName::AttributionReporting,
193 ));
194 self
195 }
196 }
197 generate_set_and_with! {
198 pub fn deny_accelerometer(mut self) -> Self {
200 self.set_directive(PermissionsPolicyDirective::deny(
201 PermissionsPolicyDirectiveName::Accelerometer,
202 ));
203 self
204 }
205 }
206 generate_set_and_with! {
207 pub fn deny_ambient_light_sensor(mut self) -> Self {
209 self.set_directive(PermissionsPolicyDirective::deny(
210 PermissionsPolicyDirectiveName::AmbientLightSensor,
211 ));
212 self
213 }
214 }
215 generate_set_and_with! {
216 pub fn deny_autoplay(mut self) -> Self {
218 self.set_directive(PermissionsPolicyDirective::deny(
219 PermissionsPolicyDirectiveName::Autoplay,
220 ));
221 self
222 }
223 }
224 generate_set_and_with! {
225 pub fn deny_battery(mut self) -> Self {
227 self.set_directive(PermissionsPolicyDirective::deny(
228 PermissionsPolicyDirectiveName::Battery,
229 ));
230 self
231 }
232 }
233 generate_set_and_with! {
234 pub fn deny_bluetooth(mut self) -> Self {
236 self.set_directive(PermissionsPolicyDirective::deny(
237 PermissionsPolicyDirectiveName::Bluetooth,
238 ));
239 self
240 }
241 }
242 generate_set_and_with! {
243 pub fn deny_display_capture(mut self) -> Self {
245 self.set_directive(PermissionsPolicyDirective::deny(
246 PermissionsPolicyDirectiveName::DisplayCapture,
247 ));
248 self
249 }
250 }
251 generate_set_and_with! {
252 pub fn deny_encrypted_media(mut self) -> Self {
254 self.set_directive(PermissionsPolicyDirective::deny(
255 PermissionsPolicyDirectiveName::EncryptedMedia,
256 ));
257 self
258 }
259 }
260 generate_set_and_with! {
261 pub fn deny_fullscreen(mut self) -> Self {
263 self.set_directive(PermissionsPolicyDirective::deny(
264 PermissionsPolicyDirectiveName::Fullscreen,
265 ));
266 self
267 }
268 }
269 generate_set_and_with! {
270 pub fn deny_gyroscope(mut self) -> Self {
272 self.set_directive(PermissionsPolicyDirective::deny(
273 PermissionsPolicyDirectiveName::Gyroscope,
274 ));
275 self
276 }
277 }
278 generate_set_and_with! {
279 pub fn deny_idle_detection(mut self) -> Self {
281 self.set_directive(PermissionsPolicyDirective::deny(
282 PermissionsPolicyDirectiveName::IdleDetection,
283 ));
284 self
285 }
286 }
287 generate_set_and_with! {
288 pub fn deny_magnetometer(mut self) -> Self {
290 self.set_directive(PermissionsPolicyDirective::deny(
291 PermissionsPolicyDirectiveName::Magnetometer,
292 ));
293 self
294 }
295 }
296 generate_set_and_with! {
297 pub fn deny_midi(mut self) -> Self {
299 self.set_directive(PermissionsPolicyDirective::deny(
300 PermissionsPolicyDirectiveName::Midi,
301 ));
302 self
303 }
304 }
305 generate_set_and_with! {
306 pub fn deny_picture_in_picture(mut self) -> Self {
308 self.set_directive(PermissionsPolicyDirective::deny(
309 PermissionsPolicyDirectiveName::PictureInPicture,
310 ));
311 self
312 }
313 }
314 generate_set_and_with! {
315 pub fn deny_publickey_credentials_get(mut self) -> Self {
317 self.set_directive(PermissionsPolicyDirective::deny(
318 PermissionsPolicyDirectiveName::PublickeyCredentialsGet,
319 ));
320 self
321 }
322 }
323 generate_set_and_with! {
324 pub fn deny_screen_wake_lock(mut self) -> Self {
326 self.set_directive(PermissionsPolicyDirective::deny(
327 PermissionsPolicyDirectiveName::ScreenWakeLock,
328 ));
329 self
330 }
331 }
332 generate_set_and_with! {
333 pub fn deny_sync_xhr(mut self) -> Self {
335 self.set_directive(PermissionsPolicyDirective::deny(
336 PermissionsPolicyDirectiveName::SyncXhr,
337 ));
338 self
339 }
340 }
341 generate_set_and_with! {
342 pub fn deny_web_share(mut self) -> Self {
344 self.set_directive(PermissionsPolicyDirective::deny(
345 PermissionsPolicyDirectiveName::WebShare,
346 ));
347 self
348 }
349 }
350 generate_set_and_with! {
351 pub fn deny_xr_spatial_tracking(mut self) -> Self {
353 self.set_directive(PermissionsPolicyDirective::deny(
354 PermissionsPolicyDirectiveName::XrSpatialTracking,
355 ));
356 self
357 }
358 }
359}
360
361impl fmt::Display for PermissionsPolicy {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 for (i, d) in self.directives.iter().enumerate() {
364 if i > 0 {
365 f.write_str(", ")?;
366 }
367 fmt::Display::fmt(d, f)?;
368 }
369 Ok(())
370 }
371}
372
373impl TypedHeader for PermissionsPolicy {
374 fn name() -> &'static HeaderName {
375 &::rama_http_types::header::PERMISSIONS_POLICY
376 }
377}
378
379impl HeaderDecode for PermissionsPolicy {
380 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
381 let mut out = Self::empty();
387 let mut any = false;
388 for value in values {
389 any = true;
390 let s = value.to_str().map_err(|_err| Error::invalid())?;
391 for raw in split_top_level_commas(s) {
392 let trimmed = raw.trim();
393 if trimmed.is_empty() {
394 continue;
395 }
396 let Some(directive) = parse_directive(trimmed) else {
397 continue;
402 };
403 out.set_directive(directive);
404 }
405 }
406 if !any {
407 return Err(Error::invalid());
408 }
409 Ok(out)
410 }
411}
412
413impl HeaderEncode for PermissionsPolicy {
414 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
415 let rendered = self.to_string();
416 match HeaderValue::try_from(rendered) {
417 Ok(v) => values.extend(::std::iter::once(v)),
418 Err(_) => {
419 values.extend(::std::iter::once(HeaderValue::from_static("")));
420 }
421 }
422 }
423}
424
425fn split_top_level_commas(s: &str) -> impl Iterator<Item = &str> {
430 let bytes = s.as_bytes();
431 let mut start = 0usize;
432 let mut depth = 0i32;
433 let mut out: Vec<&str> = Vec::new();
434 for (i, b) in bytes.iter().enumerate() {
435 match b {
436 b'(' => depth += 1,
437 b')' => depth = depth.saturating_sub(1),
438 b',' if depth == 0 => {
439 out.push(&s[start..i]);
440 start = i + 1;
441 }
442 _ => {}
443 }
444 }
445 if start <= s.len() {
446 out.push(&s[start..]);
447 }
448 out.into_iter()
449}
450
451fn parse_directive(s: &str) -> Option<PermissionsPolicyDirective> {
452 let eq = s.find('=')?;
453 let name_raw = s[..eq].trim();
454 let value_raw = s[eq + 1..].trim();
455 if name_raw.is_empty() {
456 return None;
457 }
458 let inner = value_raw
459 .strip_prefix('(')
460 .and_then(|t| t.strip_suffix(')'))?;
461 let name = PermissionsPolicyDirectiveName::from(name_raw);
466 let allow_list = inner
467 .split_whitespace()
468 .filter_map(AllowlistSource::from_token)
469 .collect();
470 Some(PermissionsPolicyDirective { name, allow_list })
471}
472
473#[cfg(test)]
474mod tests {
475 use super::super::{test_decode, test_encode};
476 use super::*;
477
478 #[test]
479 fn empty_renders_to_empty_string() {
480 let pp = PermissionsPolicy::empty();
481 assert_eq!(pp.to_string(), "");
482 }
483
484 #[test]
485 fn keyword_shortcuts_render_deny_all_chain() {
486 let pp = PermissionsPolicy::empty()
487 .with_deny_camera()
488 .with_deny_microphone()
489 .with_deny_geolocation();
490 assert_eq!(pp.to_string(), "camera=(), microphone=(), geolocation=()");
491 }
492
493 #[test]
494 fn keyword_shortcuts_share_path_with_generic_with() {
495 let via_shortcut = PermissionsPolicy::empty().with_deny_camera();
499 let via_generic = PermissionsPolicy::empty().with_directive(
500 PermissionsPolicyDirective::deny(PermissionsPolicyDirectiveName::Camera),
501 );
502 assert_eq!(via_shortcut, via_generic);
503 }
504
505 #[test]
506 fn set_mutates_in_place() {
507 let mut pp = PermissionsPolicy::empty();
508 pp.set_deny_camera();
509 pp.set_directive(PermissionsPolicyDirective::allow(
510 PermissionsPolicyDirectiveName::Microphone,
511 AllowlistSource::SelfOrigin,
512 ));
513 assert_eq!(pp.to_string(), "camera=(), microphone=(self)");
514 }
515
516 #[test]
517 fn allow_list_self_and_origin_render() {
518 let pp = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow_from(
519 PermissionsPolicyDirectiveName::Camera,
520 [
521 AllowlistSource::SelfOrigin,
522 AllowlistSource::origin("https://example.com"),
523 ],
524 ));
525 assert_eq!(pp.to_string(), r#"camera=(self "https://example.com")"#);
526 }
527
528 #[test]
529 fn wildcard_and_src_render() {
530 let pp_wild = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow(
531 PermissionsPolicyDirectiveName::Camera,
532 AllowlistSource::Wildcard,
533 ));
534 assert_eq!(pp_wild.to_string(), "camera=(*)");
535
536 let pp_src = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow(
537 PermissionsPolicyDirectiveName::Camera,
538 AllowlistSource::Src,
539 ));
540 assert_eq!(pp_src.to_string(), "camera=(src)");
541 }
542
543 #[test]
544 fn unknown_feature_via_other_round_trips() {
545 let pp = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::deny(
551 PermissionsPolicyDirectiveName::from("x-vendor-experimental"),
552 ));
553 assert_eq!(pp.to_string(), "x-vendor-experimental=()");
554 let parsed = test_decode::<PermissionsPolicy>(&[pp.to_string().as_str()]).expect("decode");
555 assert_eq!(parsed, pp);
556 }
557
558 #[test]
559 fn decode_parses_canonical_deny_all_chain() {
560 let parsed = test_decode::<PermissionsPolicy>(&[
561 "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()",
562 ])
563 .expect("decode");
564 let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
565 assert_eq!(
566 names,
567 vec![
568 "camera",
569 "microphone",
570 "geolocation",
571 "payment",
572 "usb",
573 "interest-cohort",
574 ]
575 );
576 for d in parsed.directives() {
577 assert!(
578 d.allow_list.is_empty(),
579 "{} should be deny-all",
580 d.name.as_str()
581 );
582 }
583 }
584
585 #[test]
586 fn decode_preserves_declared_order() {
587 let parsed = test_decode::<PermissionsPolicy>(&["usb=(), camera=()"]).expect("decode");
588 let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
589 assert_eq!(names, vec!["usb", "camera"]);
590 }
591
592 #[test]
593 fn decode_collapses_repeated_feature_last_wins() {
594 let parsed =
595 test_decode::<PermissionsPolicy>(&["camera=(), camera=(self)"]).expect("decode");
596 let directives: Vec<_> = parsed.directives().collect();
597 assert_eq!(directives.len(), 1);
598 assert_eq!(directives[0].name, PermissionsPolicyDirectiveName::Camera);
599 assert_eq!(
600 directives[0].allow_list.as_slice(),
601 &[AllowlistSource::SelfOrigin]
602 );
603 }
604
605 #[test]
606 fn decode_handles_multiple_header_values() {
607 let parsed = test_decode::<PermissionsPolicy>(&["camera=()", "microphone=()"])
608 .expect("decode multi-value");
609 let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
610 assert_eq!(names, vec!["camera", "microphone"]);
611 }
612
613 #[test]
614 fn decode_tolerates_whitespace() {
615 let parsed =
616 test_decode::<PermissionsPolicy>(&[" camera = ( self ) , microphone = ( ) "])
617 .expect("decode whitespace-heavy");
618 let directives: Vec<_> = parsed.directives().collect();
619 assert_eq!(directives.len(), 2);
620 assert_eq!(
621 directives[0].allow_list.as_slice(),
622 &[AllowlistSource::SelfOrigin]
623 );
624 assert!(directives[1].allow_list.is_empty());
625 }
626
627 #[test]
628 fn decode_case_insensitive_on_known_features() {
629 let parsed = test_decode::<PermissionsPolicy>(&["Camera=()"]).expect("decode");
630 let directives: Vec<_> = parsed.directives().collect();
631 assert_eq!(directives.len(), 1);
632 assert_eq!(directives[0].name, PermissionsPolicyDirectiveName::Camera);
633 }
634
635 #[test]
636 fn decode_mixed_sources() {
637 let parsed = test_decode::<PermissionsPolicy>(&[r#"camera=(self "https://a.example" *)"#])
638 .expect("decode");
639 let directives: Vec<_> = parsed.directives().collect();
640 assert_eq!(directives.len(), 1);
641 assert_eq!(
642 directives[0].allow_list.as_slice(),
643 &[
644 AllowlistSource::SelfOrigin,
645 AllowlistSource::origin("https://a.example"),
646 AllowlistSource::Wildcard,
647 ]
648 );
649 }
650
651 #[test]
652 fn decode_empty_returns_error() {
653 assert_eq!(test_decode::<PermissionsPolicy>(&[] as &[&str]), None);
654 }
655
656 #[test]
657 fn newer_feature_names_round_trip_as_typed_variants() {
658 for (token, expected) in [
662 (
663 "browsing-topics",
664 PermissionsPolicyDirectiveName::BrowsingTopics,
665 ),
666 (
667 "attribution-reporting",
668 PermissionsPolicyDirectiveName::AttributionReporting,
669 ),
670 (
671 "clipboard-read",
672 PermissionsPolicyDirectiveName::ClipboardRead,
673 ),
674 (
675 "clipboard-write",
676 PermissionsPolicyDirectiveName::ClipboardWrite,
677 ),
678 (
679 "compute-pressure",
680 PermissionsPolicyDirectiveName::ComputePressure,
681 ),
682 ("gamepad", PermissionsPolicyDirectiveName::Gamepad),
683 ("hid", PermissionsPolicyDirectiveName::Hid),
684 ("serial", PermissionsPolicyDirectiveName::Serial),
685 (
686 "storage-access",
687 PermissionsPolicyDirectiveName::StorageAccess,
688 ),
689 (
690 "publickey-credentials-create",
691 PermissionsPolicyDirectiveName::PublickeyCredentialsCreate,
692 ),
693 (
694 "window-management",
695 PermissionsPolicyDirectiveName::WindowManagement,
696 ),
697 ("local-fonts", PermissionsPolicyDirectiveName::LocalFonts),
698 ("unload", PermissionsPolicyDirectiveName::Unload),
699 ] {
700 let raw = format!("{token}=()");
701 let parsed = test_decode::<PermissionsPolicy>(&[raw.as_str()])
702 .unwrap_or_else(|| panic!("decode {token}"));
703 let directive = parsed.directives().next().expect("one directive");
704 assert_eq!(directive.name, expected, "token `{token}` parsed wrong");
705 assert_eq!(parsed.to_string(), raw, "round-trip changed `{token}`");
706 }
707 }
708
709 #[test]
710 fn topics_and_attribution_shortcuts_render_canonical_tokens() {
711 let pp = PermissionsPolicy::empty()
712 .with_deny_interest_cohort()
713 .with_deny_browsing_topics()
714 .with_deny_attribution_reporting();
715 assert_eq!(
716 pp.to_string(),
717 "interest-cohort=(), browsing-topics=(), attribution-reporting=()",
718 );
719 }
720
721 #[test]
722 fn encode_round_trips_through_header_map() {
723 let pp = PermissionsPolicy::empty()
724 .with_deny_camera()
725 .with_deny_microphone()
726 .with_directive(PermissionsPolicyDirective::allow_from(
727 PermissionsPolicyDirectiveName::Geolocation,
728 [
729 AllowlistSource::SelfOrigin,
730 AllowlistSource::origin("https://example.com"),
731 ],
732 ));
733 let map = test_encode(pp.clone());
734 let raw = map
735 .get(PermissionsPolicy::name())
736 .expect("set")
737 .to_str()
738 .unwrap()
739 .to_owned();
740 assert_eq!(raw, pp.to_string());
741 let parsed = test_decode::<PermissionsPolicy>(&[raw.as_str()]).expect("decode");
742 assert_eq!(parsed, pp);
743 }
744}