Skip to main content

secret_service/
collection.rs

1use crate::Error;
2use crate::Item;
3use crate::proxy::collection::CollectionProxy;
4use crate::proxy::service::ServiceProxy;
5use crate::session::Session;
6use crate::ss::{SS_DBUS_NAME, SS_ITEM_ATTRIBUTES, SS_ITEM_LABEL};
7use crate::util::{LockAction, exec_prompt, format_secret, lock_or_unlock};
8
9use std::collections::HashMap;
10use zbus::{
11    proxy::CacheProperties,
12    zvariant::{Dict, ObjectPath, OwnedObjectPath, Value},
13};
14
15// Collection struct.
16// Should always be created from the SecretService entry point,
17// whether through a new collection or a collection search
18pub struct Collection<'a> {
19    conn: zbus::Connection,
20    session: &'a Session,
21    pub collection_path: OwnedObjectPath,
22    collection_proxy: CollectionProxy<'a>,
23    service_proxy: &'a ServiceProxy<'a>,
24}
25
26impl<'a> Collection<'a> {
27    pub(crate) async fn new(
28        conn: zbus::Connection,
29        session: &'a Session,
30        service_proxy: &'a ServiceProxy<'_>,
31        collection_path: OwnedObjectPath,
32    ) -> Result<Collection<'a>, Error> {
33        let collection_proxy = CollectionProxy::builder(&conn)
34            .destination(SS_DBUS_NAME)?
35            .path(collection_path.clone())?
36            .cache_properties(CacheProperties::No)
37            .build()
38            .await?;
39
40        Ok(Collection {
41            conn,
42            session,
43            collection_path,
44            collection_proxy,
45            service_proxy,
46        })
47    }
48
49    pub async fn is_locked(&self) -> Result<bool, Error> {
50        Ok(self.collection_proxy.locked().await?)
51    }
52
53    pub async fn ensure_unlocked(&self) -> Result<(), Error> {
54        if self.is_locked().await? {
55            Err(Error::Locked)
56        } else {
57            Ok(())
58        }
59    }
60
61    pub async fn unlock(&self) -> Result<(), Error> {
62        lock_or_unlock(
63            self.conn.clone(),
64            self.service_proxy,
65            &self.collection_path,
66            LockAction::Unlock,
67        )
68        .await
69    }
70
71    pub async fn lock(&self) -> Result<(), Error> {
72        lock_or_unlock(
73            self.conn.clone(),
74            self.service_proxy,
75            &self.collection_path,
76            LockAction::Lock,
77        )
78        .await
79    }
80
81    /// Deletes dbus object, but struct instance still exists (current implementation)
82    pub async fn delete(&self) -> Result<(), Error> {
83        // ensure_unlocked handles prompt for unlocking if necessary
84        self.ensure_unlocked().await?;
85        let prompt_path = self.collection_proxy.delete().await?;
86
87        // "/" means no prompt necessary
88        if prompt_path.as_str() != "/" {
89            exec_prompt(self.conn.clone(), &prompt_path).await?;
90        }
91
92        Ok(())
93    }
94
95    pub async fn get_all_items(&self) -> Result<Vec<Item<'_>>, Error> {
96        let items = self.collection_proxy.items().await?;
97
98        // map array of item paths to Item
99        futures_util::future::join_all(items.into_iter().map(|item_path| {
100            Item::new(
101                self.conn.clone(),
102                self.session,
103                self.service_proxy,
104                item_path.into(),
105            )
106        }))
107        .await
108        .into_iter()
109        .collect::<Result<_, _>>()
110    }
111
112    pub async fn search_items(
113        &self,
114        attributes: HashMap<&str, &str>,
115    ) -> Result<Vec<Item<'_>>, Error> {
116        let items = self.collection_proxy.search_items(attributes).await?;
117
118        // map array of item paths to Item
119        futures_util::future::join_all(items.into_iter().map(|item_path| {
120            Item::new(
121                self.conn.clone(),
122                self.session,
123                self.service_proxy,
124                item_path,
125            )
126        }))
127        .await
128        .into_iter()
129        .collect::<Result<_, _>>()
130    }
131
132    pub async fn get_label(&self) -> Result<String, Error> {
133        Ok(self.collection_proxy.label().await?)
134    }
135
136    pub async fn set_label(&self, new_label: &str) -> Result<(), Error> {
137        Ok(self.collection_proxy.set_label(new_label).await?)
138    }
139
140    pub async fn create_item(
141        &self,
142        label: &str,
143        attributes: HashMap<&str, &str>,
144        secret: &[u8],
145        replace: bool,
146        content_type: &str,
147    ) -> Result<Item<'_>, Error> {
148        let secret_struct = format_secret(self.session, secret, content_type)?;
149
150        let mut properties: HashMap<&str, Value> = HashMap::new();
151        let attributes: Dict = attributes.into();
152
153        properties.insert(SS_ITEM_LABEL, label.into());
154        properties.insert(SS_ITEM_ATTRIBUTES, attributes.into());
155
156        let created_item = self
157            .collection_proxy
158            .create_item(properties, secret_struct, replace)
159            .await?;
160
161        // This prompt handling is practically identical to create_collection
162        let item_path: ObjectPath = {
163            // Get path of created object
164            let created_path = created_item.item;
165
166            // Check if that path is "/", if so should execute a prompt
167            if created_path.as_str() == "/" {
168                let prompt_path = created_item.prompt;
169
170                // Exec prompt and parse result
171                let prompt_res = exec_prompt(self.conn.clone(), &prompt_path).await?;
172                prompt_res.try_into()?
173            } else {
174                // if not, just return created path
175                created_path.into()
176            }
177        };
178
179        Item::new(
180            self.conn.clone(),
181            self.session,
182            self.service_proxy,
183            item_path.into(),
184        )
185        .await
186    }
187}
188
189#[cfg(test)]
190mod test {
191    use crate::*;
192
193    #[tokio::test]
194    async fn should_create_collection_struct() {
195        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
196        let _ = ss.get_default_collection().await.unwrap();
197        // tested under SecretService struct
198    }
199
200    #[tokio::test]
201    async fn should_check_if_collection_locked() {
202        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
203        let collection = ss.get_default_collection().await.unwrap();
204        let _ = collection.is_locked().await.unwrap();
205    }
206
207    #[tokio::test]
208    #[ignore] // should unignore this test this manually, otherwise will constantly prompt during tests.
209    async fn should_lock_and_unlock() {
210        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
211        let collection = ss.get_default_collection().await.unwrap();
212        let locked = collection.is_locked().await.unwrap();
213        if locked {
214            collection.unlock().await.unwrap();
215            collection.ensure_unlocked().await.unwrap();
216            assert!(!collection.is_locked().await.unwrap());
217            collection.lock().await.unwrap();
218            assert!(collection.is_locked().await.unwrap());
219        } else {
220            collection.lock().await.unwrap();
221            assert!(collection.is_locked().await.unwrap());
222            collection.unlock().await.unwrap();
223            collection.ensure_unlocked().await.unwrap();
224            assert!(!collection.is_locked().await.unwrap());
225        }
226    }
227
228    #[tokio::test]
229    #[ignore]
230    async fn should_delete_collection() {
231        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
232        let collections = ss.get_all_collections().await.unwrap();
233        let count_before = collections.len();
234        for collection in collections {
235            let collection_path = &*collection.collection_path;
236            if collection_path.contains("Test") {
237                collection.unlock().await.unwrap();
238                collection.delete().await.unwrap();
239            }
240        }
241        //double check after
242        let collections = ss.get_all_collections().await.unwrap();
243        assert!(
244            collections.len() < count_before,
245            "collections before delete {count_before}"
246        );
247    }
248
249    #[tokio::test]
250    async fn should_get_all_items() {
251        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
252        let collection = ss.get_default_collection().await.unwrap();
253        collection.get_all_items().await.unwrap();
254    }
255
256    #[tokio::test]
257    async fn should_search_items() {
258        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
259        let collection = ss.get_default_collection().await.unwrap();
260
261        // Create an item
262        let item = collection
263            .create_item(
264                "test",
265                HashMap::from([("test_attributes_in_collection", "test")]),
266                b"test_secret",
267                false,
268                "text/plain",
269            )
270            .await
271            .unwrap();
272
273        // handle empty vec search
274        collection.search_items(HashMap::new()).await.unwrap();
275
276        // handle no result
277        let bad_search = collection
278            .search_items(HashMap::from([("test_bad", "test")]))
279            .await
280            .unwrap();
281        assert_eq!(bad_search.len(), 0);
282
283        // handle correct search for item and compare
284        let search_item = collection
285            .search_items(HashMap::from([("test_attributes_in_collection", "test")]))
286            .await
287            .unwrap();
288
289        assert_eq!(item.item_path, search_item[0].item_path);
290        item.delete().await.unwrap();
291    }
292
293    #[tokio::test]
294    #[ignore]
295    async fn should_get_and_set_collection_label() {
296        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
297        let collection = ss.get_default_collection().await.unwrap();
298        let label = collection.get_label().await.unwrap();
299        assert_eq!(label, "Login");
300
301        // Set label to test and check
302        collection.unlock().await.unwrap();
303        collection.set_label("Test").await.unwrap();
304        let label = collection.get_label().await.unwrap();
305        assert_eq!(label, "Test");
306
307        // Reset label to original and test
308        collection.unlock().await.unwrap();
309        collection.set_label("Login").await.unwrap();
310        let label = collection.get_label().await.unwrap();
311        assert_eq!(label, "Login");
312
313        collection.lock().await.unwrap();
314    }
315
316    #[tokio::test]
317    async fn should_get_collection_by_path() {
318        let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
319        let collection = ss.get_default_collection().await.unwrap();
320        let label = collection.get_label().await.unwrap();
321
322        // get collection by path
323        let path = collection.collection_path.clone();
324        let collection_prime = ss.get_collection_by_path(path).await.unwrap();
325        let label_prime = collection_prime.get_label().await.unwrap();
326        assert_eq!(label, label_prime);
327    }
328}