1
use std::path::Path;
2

            
3
use 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.
10
pub 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.
27
2
async fn migrate_inner(
28
    service: &Service,
29
    secret: Secret,
30
    keyring_path: &Path,
31
    attributes: Vec<impl AsAttributes>,
32
    replace: bool,
33
) -> Result<()> {
34
6
    let file_backend = UnlockedKeyring::load(keyring_path, secret).await?;
35

            
36
6
    let collection = service.default_collection().await?;
37
2
    let mut all_items = Vec::default();
38

            
39
8
    for attrs in attributes {
40
10
        let items = collection.search_items(&attrs).await?;
41
2
        all_items.extend(items);
42
    }
43
4
    let mut new_items = Vec::with_capacity(all_items.capacity());
44

            
45
6
    for item in all_items.iter() {
46
8
        let attributes = item.attributes().await?;
47
8
        let label = item.label().await?;
48
8
        let secret = item.secret().await?;
49

            
50
2
        new_items.push((label, attributes, secret, replace));
51
    }
52

            
53
4
    file_backend.create_items(new_items).await?;
54

            
55
    // Delete items from source after successful creation in destination
56
2
    let mut deletion_errors = Vec::new();
57
8
    for item in all_items.iter() {
58
8
        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
2
    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
2
    Ok(())
74
}
75

            
76
#[cfg(test)]
77
mod 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
}