fix(permissions): make profile snapshots authoritative

This commit is contained in:
2026-07-09 21:56:36 -04:00
parent c28ec2bee8
commit b422ac1631
47 changed files with 1918 additions and 403 deletions
+8
View File
@@ -143,6 +143,12 @@ pub struct BrowserSnapshot {
pub command_query: String,
}
#[derive(Debug)]
struct TransferredSitePermission {
entry: SitePermissionEntry,
grant_revision: u64,
}
#[derive(Debug)]
pub struct BrowserCore {
spaces: Vec<Space>,
@@ -153,6 +159,7 @@ pub struct BrowserCore {
notes: Vec<NoteEntry>,
reading_list: Vec<ReadingListEntry>,
site_permissions: Vec<SitePermissionEntry>,
transferred_site_permissions: Vec<TransferredSitePermission>,
site_permission_audit_events: Vec<SitePermissionAuditEvent>,
download_entries: Vec<DownloadEntry>,
history_entries: Vec<HistoryEntry>,
@@ -238,6 +245,7 @@ impl BrowserCore {
notes: Vec::new(),
reading_list: Vec::new(),
site_permissions: Vec::new(),
transferred_site_permissions: Vec::new(),
site_permission_audit_events: Vec::new(),
download_entries: Vec::new(),
history_entries: Vec::new(),
@@ -224,6 +224,8 @@ pub(super) struct ElyLocalSitePermissionAuditRecord {
enum ElyLocalSitePermissionAuditActionRecord {
Set { decision: String },
Revoked,
Transferred,
Consumed,
}
impl ElyLocalProfileRecord {
@@ -436,6 +438,8 @@ impl ElyLocalSitePermissionAuditActionRecord {
Self::Set { decision: decision.as_str().to_string() }
}
SitePermissionAuditAction::Revoked => Self::Revoked,
SitePermissionAuditAction::Transferred => Self::Transferred,
SitePermissionAuditAction::Consumed => Self::Consumed,
}
}
}
@@ -11,7 +11,7 @@ use super::local_data_export_records::{
};
use crate::CoreError;
pub const ELYDATA_SCHEMA_VERSION: u16 = 1;
pub const ELYDATA_SCHEMA_VERSION: u16 = 2;
pub const ELYDATA_FILE_EXTENSION: &str = "elydata";
impl BrowserCore {
@@ -79,6 +79,7 @@ impl BrowserCore {
.site_permissions
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.filter(|entry| entry.decision() != ely_domain::SitePermissionDecision::AllowOnce)
.map(ElyLocalSitePermissionRecord::from_site_permission)
.collect(),
site_permission_audit_events: self
@@ -132,6 +132,7 @@ impl BrowserCore {
.site_permissions
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.filter(|entry| entry.decision() != ely_domain::SitePermissionDecision::AllowOnce)
.count(),
site_permission_audit_events: self
.site_permission_audit_events
@@ -75,6 +75,15 @@ impl BrowserCore {
true
}
});
self.transferred_site_permissions.retain(|permission| {
if permission.entry.profile_id() == profile_id && permission.entry.origin() == origin {
revoked_permissions
.push((permission.entry.origin().clone(), permission.entry.feature()));
false
} else {
true
}
});
let revoked_count = revoked_permissions.len();
for (origin, feature) in revoked_permissions {
@@ -7,7 +7,7 @@ use ely_domain::{
use crate::CoreError;
use super::BrowserCore;
use super::{BrowserCore, TransferredSitePermission};
impl BrowserCore {
pub fn set_site_permission(
@@ -45,21 +45,112 @@ impl BrowserCore {
pub(super) fn visible_site_permissions(&self) -> Vec<SitePermissionEntry> {
self.site_permissions
.iter()
.chain(self.transferred_site_permissions.iter().map(|permission| &permission.entry))
.filter(|entry| entry.profile_id() == &self.active_profile_id)
.cloned()
.collect()
}
pub fn site_permissions_for_profile_origin(
pub fn site_permissions_for_profile(&self, profile_id: &ProfileId) -> Vec<SitePermissionEntry> {
self.site_permissions
.iter()
.filter(|entry| entry.profile_id() == profile_id)
.cloned()
.collect()
}
pub fn transferred_site_permissions_for_profile(
&self,
profile_id: &ProfileId,
) -> Vec<(SitePermissionEntry, u64)> {
self.transferred_site_permissions
.iter()
.filter(|permission| permission.entry.profile_id() == profile_id)
.map(|permission| (permission.entry.clone(), permission.grant_revision))
.collect()
}
pub fn site_permission_audit_events_for_profile(
&self,
profile_id: &ProfileId,
) -> Vec<SitePermissionAuditEvent> {
self.site_permission_audit_events
.iter()
.filter(|event| event.profile_id() == profile_id)
.cloned()
.collect()
}
pub fn site_permission_revision(
&self,
profile_id: &ProfileId,
origin: &SiteOrigin,
) -> Vec<SitePermissionEntry> {
self.site_permissions
feature: SitePermissionFeature,
) -> u64 {
self.site_permission_audit_events
.iter()
.filter(|entry| entry.profile_id() == profile_id && entry.origin() == origin)
.cloned()
.collect()
.filter(|event| {
event.profile_id() == profile_id
&& event.origin() == origin
&& event.feature() == feature
})
.fold(0_u64, |revision, _| revision.saturating_add(1))
}
pub fn transfer_site_permission_once(
&mut self,
profile_id: &ProfileId,
origin: &SiteOrigin,
feature: SitePermissionFeature,
revision: u64,
) -> Result<bool, CoreError> {
self.require_profile(profile_id)?;
if self.site_permission_revision(profile_id, origin, feature) != revision {
return Ok(false);
}
let Some(index) = self.site_permission_entry_index(profile_id, origin, feature) else {
return Ok(false);
};
if self.site_permissions[index].decision() != SitePermissionDecision::AllowOnce {
return Ok(false);
}
let entry = self.site_permissions.remove(index);
self.record_site_permission_audit_event(
profile_id.clone(),
entry.origin().clone(),
feature,
SitePermissionAuditAction::Transferred,
);
self.transferred_site_permissions
.push(TransferredSitePermission { entry, grant_revision: revision });
Ok(true)
}
pub fn finish_site_permission_once(
&mut self,
profile_id: &ProfileId,
origin: &SiteOrigin,
feature: SitePermissionFeature,
grant_revision: u64,
) -> Result<bool, CoreError> {
self.require_profile(profile_id)?;
let Some(index) = self.transferred_site_permissions.iter().position(|permission| {
permission.entry.profile_id() == profile_id
&& permission.entry.origin() == origin
&& permission.entry.feature() == feature
&& permission.grant_revision == grant_revision
}) else {
return Ok(false);
};
let permission = self.transferred_site_permissions.remove(index);
self.record_site_permission_audit_event(
profile_id.clone(),
permission.entry.origin().clone(),
feature,
SitePermissionAuditAction::Consumed,
);
Ok(true)
}
pub(super) fn visible_site_permission_audit_events(&self) -> Vec<SitePermissionAuditEvent> {
@@ -79,6 +170,12 @@ impl BrowserCore {
) -> Result<(), CoreError> {
self.require_profile(profile_id)?;
self.transferred_site_permissions.retain(|permission| {
permission.entry.profile_id() != profile_id
|| permission.entry.origin() != &origin
|| permission.entry.feature() != feature
});
if let Some(entry) = self.site_permission_entry_mut(profile_id, &origin, feature) {
if entry.decision() == decision {
return Ok(());
@@ -109,11 +206,20 @@ impl BrowserCore {
feature: SitePermissionFeature,
) -> Result<(), CoreError> {
self.require_profile(profile_id)?;
let Some(index) = self.site_permission_entry_index(profile_id, origin, feature) else {
return Ok(());
};
let entry = self.site_permissions.remove(index);
let entry =
if let Some(index) = self.site_permission_entry_index(profile_id, origin, feature) {
self.site_permissions.remove(index)
} else if let Some(index) =
self.transferred_site_permissions.iter().position(|permission| {
permission.entry.profile_id() == profile_id
&& permission.entry.origin() == origin
&& permission.entry.feature() == feature
})
{
self.transferred_site_permissions.remove(index).entry
} else {
return Ok(());
};
self.record_site_permission_audit_event(
profile_id.clone(),
entry.origin().clone(),
@@ -138,6 +244,15 @@ impl BrowserCore {
true
}
});
self.transferred_site_permissions.retain(|permission| {
if permission.entry.profile_id() == profile_id {
revoked_permissions
.push((permission.entry.origin().clone(), permission.entry.feature()));
false
} else {
true
}
});
let revoked_count = revoked_permissions.len();
for (origin, feature) in revoked_permissions {
@@ -15,6 +15,7 @@ impl BrowserCore {
self.site_permissions
.iter()
.filter(|entry| self.profile_allows_cloud_sync(entry.profile_id()))
.filter(|entry| entry.decision() != SitePermissionDecision::AllowOnce)
.collect()
}
@@ -30,6 +31,15 @@ impl BrowserCore {
SitePermissionFeature::parse(&record.feature).map_err(snapshot_schema_error)?;
let decision =
SitePermissionDecision::parse(&record.decision).map_err(snapshot_schema_error)?;
if decision == SitePermissionDecision::AllowOnce {
summary.record_skipped();
return Ok(());
}
self.transferred_site_permissions.retain(|permission| {
permission.entry.profile_id() != &profile_id
|| permission.entry.origin() != &origin
|| permission.entry.feature() != feature
});
let existing_index = self.site_permissions.iter().position(|entry| {
entry.profile_id() == &profile_id
&& entry.origin() == &origin
@@ -51,3 +61,32 @@ impl BrowserCore {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{InitialBrowserConfig, sync_engine::SyncSnapshotApplySummary};
#[test]
fn legacy_allow_once_sync_record_is_skipped() -> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let profile_id = core.snapshot()?.active_profile_id;
let record = SitePermissionSyncRecord {
profile_id: profile_id.as_str().to_string(),
origin: "https://example.com".to_string(),
feature: "camera".to_string(),
decision: "allow-once".to_string(),
};
let mut summary = SyncSnapshotApplySummary::default();
core.apply_site_permission_sync_record(
record,
&mut summary,
&SyncSnapshotApplyContext::default(),
)?;
assert_eq!(summary.skipped(), 1);
assert!(core.snapshot()?.site_permissions.is_empty());
Ok(())
}
}