Skip to main content

oo7/
migration.rs

1use std::path::Path;
2
3use crate::{AsAttributes, Result, Secret, dbus::Service, file::UnlockedKeyring};
4
5/// Helper to migrate your secrets from the host Secret Service
6/// to the sandboxed file backend.
7///
8/// If the migration is successful, the items are removed from the host
9/// Secret Service.
10pub async fn migrate(attributes: Vec<impl AsAttributes>, replace: bool) -> Result<()> {
11    let service = Service::new().await?;
12    let secret = match Secret::sandboxed().await {
13        Ok(secret) => Ok(secret),
14        Err(super::file::Error::Portal(ashpd::Error::PortalNotFound(_))) => {
15            #[cfg(feature = "tracing")]
16            tracing::debug!("Portal not available, no migration to do");
17            return Ok(());
18        }
19        Err(err) => Err(err),
20    }?;
21    let keyring_path = crate::file::api::Keyring::default_path()?;
22
23    migrate_inner(&service, secret, &keyring_path, attributes, replace).await
24}
25
26/// Inner migration function for testing.
27async fn migrate_inner(
28    service: &Service,
29    secret: Secret,
30    keyring_path: &Path,
31    attributes: Vec<impl AsAttributes>,
32    replace: bool,
33) -> Result<()> {
34    let file_backend = UnlockedKeyring::load(keyring_path, secret).await?;
35
36    let collection = service.default_collection().await?;
37    let mut all_items = Vec::default();
38
39    for attrs in attributes {
40        let items = collection.search_items(&attrs).await?;
41        all_items.extend(items);
42    }
43    let mut new_items = Vec::with_capacity(all_items.capacity());
44
45    for item in all_items.iter() {
46        let attributes = item.attributes().await?;
47        let label = item.label().await?;
48        let secret = item.secret().await?;
49
50        new_items.push((label, attributes, secret, replace));
51    }
52
53    file_backend.create_items(new_items).await?;
54
55    // Delete items from source after successful creation in destination
56    let mut deletion_errors = Vec::new();
57    for item in all_items.iter() {
58        if let Err(e) = item.delete(None).await {
59            deletion_errors.push(e);
60        }
61    }
62
63    // Report deletion failures - partial migration is still an error condition
64    if !deletion_errors.is_empty() {
65        #[cfg(feature = "tracing")]
66        tracing::error!(
67            "Migration partially failed: {} items could not be deleted from source",
68            deletion_errors.len()
69        );
70        return Err(deletion_errors.into_iter().next().unwrap().into());
71    }
72
73    Ok(())
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::{Secret, dbus::Service, file::UnlockedKeyring};
80
81    #[tokio::test]
82    #[cfg(feature = "tokio")]
83    async fn test_migrate_from_dbus_to_file() {
84        let temp_dir = tempfile::tempdir().unwrap();
85        let setup = oo7_daemon::tests::TestServiceSetup::plain_session(true)
86            .await
87            .unwrap();
88
89        // Create a DBus service with test connection
90        let service = Service::new_with_connection(&setup.client_conn)
91            .await
92            .unwrap();
93
94        // Create some items on the DBus backend
95        let collection = service.default_collection().await.unwrap();
96
97        collection
98            .create_item(
99                "Migration Test 1",
100                &[("app", "test-migration"), ("user", "alice")],
101                "secret1",
102                false,
103                None,
104            )
105            .await
106            .unwrap();
107
108        collection
109            .create_item(
110                "Migration Test 2",
111                &[("app", "test-migration"), ("user", "bob")],
112                "secret2",
113                false,
114                None,
115            )
116            .await
117            .unwrap();
118
119        // Verify items exist in DBus backend
120        let items_before = collection
121            .search_items(&[("app", "test-migration")])
122            .await
123            .unwrap();
124        assert_eq!(items_before.len(), 2);
125
126        // Create file backend keyring
127        let keyring_path = temp_dir.path().join("migrated.keyring");
128        let secret = Secret::from([1, 2].into_iter().cycle().take(64).collect::<Vec<_>>());
129
130        // Perform migration using internal function
131        migrate_inner(
132            &service,
133            secret.clone(),
134            &keyring_path,
135            vec![&[("app", "test-migration")]],
136            false,
137        )
138        .await
139        .unwrap();
140
141        // Verify items are deleted from DBus backend
142        let items_after = collection
143            .search_items(&[("app", "test-migration")])
144            .await
145            .unwrap();
146        assert_eq!(items_after.len(), 0);
147
148        // Verify items exist in file backend
149        let file_backend = UnlockedKeyring::load(&keyring_path, secret).await.unwrap();
150        let migrated_items = file_backend
151            .search_items(&[("app", "test-migration")])
152            .await
153            .unwrap();
154
155        assert_eq!(migrated_items.len(), 2);
156
157        // Verify item details
158        let alice_item = migrated_items
159            .iter()
160            .find(|item| {
161                item.attributes()
162                    .get("user")
163                    .map(|u| u == "alice")
164                    .unwrap_or(false)
165            })
166            .expect("Alice's item should exist");
167
168        assert_eq!(alice_item.label(), "Migration Test 1");
169        assert_eq!(alice_item.secret(), Secret::text("secret1"));
170        assert_eq!(
171            alice_item.attributes().get("app").unwrap(),
172            "test-migration"
173        );
174
175        let bob_item = migrated_items
176            .iter()
177            .find(|item| {
178                item.attributes()
179                    .get("user")
180                    .map(|u| u == "bob")
181                    .unwrap_or(false)
182            })
183            .expect("Bob's item should exist");
184
185        assert_eq!(bob_item.label(), "Migration Test 2");
186        assert_eq!(bob_item.secret(), Secret::text("secret2"));
187        assert_eq!(bob_item.attributes().get("app").unwrap(), "test-migration");
188    }
189}