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, Some(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, Some(secret))
150
            .await
151
            .unwrap();
152
        let migrated_items = file_backend
153
            .search_items(&[("app", "test-migration")])
154
            .await
155
            .unwrap();
156

            
157
        assert_eq!(migrated_items.len(), 2);
158

            
159
        // Verify item details
160
        let alice_item = migrated_items
161
            .iter()
162
            .find(|item| {
163
                item.attributes()
164
                    .get("user")
165
                    .map(|u| u == "alice")
166
                    .unwrap_or(false)
167
            })
168
            .expect("Alice's item should exist");
169

            
170
        assert_eq!(alice_item.label(), "Migration Test 1");
171
        assert_eq!(alice_item.secret(), Secret::text("secret1"));
172
        assert_eq!(
173
            alice_item.attributes().get("app").unwrap(),
174
            "test-migration"
175
        );
176

            
177
        let bob_item = migrated_items
178
            .iter()
179
            .find(|item| {
180
                item.attributes()
181
                    .get("user")
182
                    .map(|u| u == "bob")
183
                    .unwrap_or(false)
184
            })
185
            .expect("Bob's item should exist");
186

            
187
        assert_eq!(bob_item.label(), "Migration Test 2");
188
        assert_eq!(bob_item.secret(), Secret::text("secret2"));
189
        assert_eq!(bob_item.attributes().get("app").unwrap(), "test-migration");
190
    }
191
}