Scope Servo permissions by profile origin

This commit is contained in:
2026-05-08 23:47:05 -04:00
parent 31162da330
commit 4ed1666ece
6 changed files with 243 additions and 42 deletions
@@ -1,20 +1,192 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use ely_domain::{ProfileId, TabId};
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
use servo::WebView;
use crate::PermissionDecision;
use crate::{PermissionDecision, PermissionRequest};
pub(super) type PermissionStore = Rc<RefCell<HashMap<PermissionKey, PermissionDecision>>>;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(super) struct PermissionKey {
profile_id: ProfileId,
tab_id: TabId,
feature: String,
origin: SiteOrigin,
feature: SitePermissionFeature,
}
impl PermissionKey {
pub(super) fn new(profile_id: ProfileId, tab_id: TabId, feature: String) -> Self {
Self { profile_id, tab_id, feature }
fn new(profile_id: ProfileId, origin: SiteOrigin, feature: SitePermissionFeature) -> Self {
Self { profile_id, origin, feature }
}
}
pub(super) fn set_permission_decision(
permissions: &PermissionStore,
request: PermissionRequest,
decision: PermissionDecision,
) {
permissions
.borrow_mut()
.insert(PermissionKey::new(request.profile_id, request.origin, request.feature), decision);
}
pub(super) fn permission_decision_for_webview(
permissions: &PermissionStore,
profile_id: &ProfileId,
webview: &WebView,
fallback_url: Option<String>,
servo_feature: servo::PermissionFeature,
) -> Option<PermissionDecision> {
let origin = site_origin_for_webview(webview, fallback_url)?;
let feature = site_permission_feature_for_servo(servo_feature)?;
take_permission_decision(permissions, profile_id, origin, feature)
}
fn take_permission_decision(
permissions: &PermissionStore,
profile_id: &ProfileId,
origin: SiteOrigin,
feature: SitePermissionFeature,
) -> Option<PermissionDecision> {
let key = PermissionKey::new(profile_id.clone(), origin, feature);
let mut permissions = permissions.borrow_mut();
match permissions.get(&key).cloned() {
Some(PermissionDecision::AllowOnce) => permissions.remove(&key),
decision => decision,
}
}
fn site_origin_for_webview(webview: &WebView, fallback_url: Option<String>) -> Option<SiteOrigin> {
webview
.url()
.map(|url| url.to_string())
.or(fallback_url)
.and_then(|url| SiteOrigin::parse(url).ok())
}
fn site_permission_feature_for_servo(
feature: servo::PermissionFeature,
) -> Option<SitePermissionFeature> {
match feature {
servo::PermissionFeature::Geolocation => Some(SitePermissionFeature::Location),
servo::PermissionFeature::Notifications => Some(SitePermissionFeature::Notifications),
servo::PermissionFeature::Camera => Some(SitePermissionFeature::Camera),
servo::PermissionFeature::Microphone => Some(SitePermissionFeature::Microphone),
servo::PermissionFeature::PersistentStorage => {
Some(SitePermissionFeature::StoragePersistence)
}
servo::PermissionFeature::Push
| servo::PermissionFeature::Midi
| servo::PermissionFeature::Speaker
| servo::PermissionFeature::DeviceInfo
| servo::PermissionFeature::BackgroundSync
| servo::PermissionFeature::Bluetooth => None,
}
}
#[cfg(test)]
mod tests {
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
use crate::{PermissionDecision, PermissionRequest};
use super::{PermissionStore, set_permission_decision, take_permission_decision};
#[test]
fn allow_once_is_consumed_after_one_matching_origin_request()
-> Result<(), Box<dyn std::error::Error>> {
let permissions = PermissionStore::default();
let profile_id = ProfileId::new();
let origin = SiteOrigin::parse("https://example.com/path")?;
set_permission_decision(
&permissions,
PermissionRequest {
webview_id: ely_domain::WebViewId::new(),
profile_id: profile_id.clone(),
origin: origin.clone(),
feature: SitePermissionFeature::Camera,
},
PermissionDecision::AllowOnce,
);
assert_eq!(
take_permission_decision(
&permissions,
&profile_id,
origin.clone(),
SitePermissionFeature::Camera,
),
Some(PermissionDecision::AllowOnce)
);
assert_eq!(
take_permission_decision(
&permissions,
&profile_id,
origin,
SitePermissionFeature::Camera
),
None
);
Ok(())
}
#[test]
fn allow_always_stays_scoped_to_profile_origin_and_feature()
-> Result<(), Box<dyn std::error::Error>> {
let permissions = PermissionStore::default();
let profile_id = ProfileId::new();
let other_profile_id = ProfileId::new();
let origin = SiteOrigin::parse("https://example.com")?;
let other_origin = SiteOrigin::parse("https://example.org")?;
set_permission_decision(
&permissions,
PermissionRequest {
webview_id: ely_domain::WebViewId::new(),
profile_id: profile_id.clone(),
origin: origin.clone(),
feature: SitePermissionFeature::Notifications,
},
PermissionDecision::AllowAlways,
);
assert_eq!(
take_permission_decision(
&permissions,
&profile_id,
origin.clone(),
SitePermissionFeature::Notifications,
),
Some(PermissionDecision::AllowAlways)
);
assert_eq!(
take_permission_decision(
&permissions,
&other_profile_id,
origin,
SitePermissionFeature::Notifications,
),
None
);
assert_eq!(
take_permission_decision(
&permissions,
&profile_id,
other_origin,
SitePermissionFeature::Notifications,
),
None
);
assert_eq!(
take_permission_decision(
&permissions,
&profile_id,
SiteOrigin::parse("https://example.com")?,
SitePermissionFeature::Camera,
),
None
);
Ok(())
}
}