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
15pub 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 pub async fn delete(&self) -> Result<(), Error> {
83 self.ensure_unlocked().await?;
85 let prompt_path = self.collection_proxy.delete().await?;
86
87 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 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 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 let item_path: ObjectPath = {
163 let created_path = created_item.item;
165
166 if created_path.as_str() == "/" {
168 let prompt_path = created_item.prompt;
169
170 let prompt_res = exec_prompt(self.conn.clone(), &prompt_path).await?;
172 prompt_res.try_into()?
173 } else {
174 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 }
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] 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 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 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 collection.search_items(HashMap::new()).await.unwrap();
275
276 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 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 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 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 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}