pub mod blocking;
mod error;
mod proxy;
mod session;
mod ss;
mod util;
mod collection;
pub use collection::Collection;
pub use error::Error;
mod item;
pub use item::Item;
pub use session::EncryptionType;
use crate::proxy::service::ServiceProxy;
use crate::session::Session;
use crate::ss::SS_COLLECTION_LABEL;
use crate::util::exec_prompt;
use futures_util::TryFutureExt;
use std::collections::HashMap;
use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value};
pub struct SecretService<'a> {
conn: zbus::Connection,
session: Session,
service_proxy: ServiceProxy<'a>,
}
pub struct SearchItemsResult<T> {
pub unlocked: Vec<T>,
pub locked: Vec<T>,
}
impl<'a> SecretService<'a> {
pub async fn connect(encryption: EncryptionType) -> Result<SecretService<'a>, Error> {
let conn = zbus::Connection::session()
.await
.map_err(util::handle_conn_error)?;
Self::connect_with_existing(encryption, conn).await
}
pub async fn connect_with_existing(
encryption: EncryptionType,
session_conn: zbus::Connection,
) -> Result<SecretService<'a>, Error> {
let service_proxy = ServiceProxy::new(&session_conn)
.await
.map_err(util::handle_conn_error)?;
let session = Session::new(&service_proxy, encryption).await?;
Ok(SecretService {
conn: session_conn,
session,
service_proxy,
})
}
pub async fn get_all_collections(&self) -> Result<Vec<Collection<'_>>, Error> {
let collections = self.service_proxy.collections().await?;
futures_util::future::join_all(collections.into_iter().map(|object_path| {
Collection::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
object_path.into(),
)
}))
.await
.into_iter()
.collect::<Result<_, _>>()
}
pub async fn get_collection_by_alias(&self, alias: &str) -> Result<Collection<'_>, Error> {
let object_path = self.service_proxy.read_alias(alias).await?;
if object_path.as_str() == "/" {
Err(Error::NoResult)
} else {
Collection::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
object_path,
)
.await
}
}
pub async fn get_default_collection(&self) -> Result<Collection<'_>, Error> {
self.get_collection_by_alias("default").await
}
pub async fn get_any_collection(&self) -> Result<Collection<'_>, Error> {
self.get_default_collection()
.or_else(|_| self.get_collection_by_alias("session"))
.or_else(|_| async {
let mut collections = self.get_all_collections().await?;
if collections.is_empty() {
Err(Error::NoResult)
} else {
Ok(collections.swap_remove(0))
}
})
.await
}
pub async fn create_collection(
&self,
label: &str,
alias: &str,
) -> Result<Collection<'_>, Error> {
let mut properties: HashMap<&str, Value> = HashMap::new();
properties.insert(SS_COLLECTION_LABEL, label.into());
let created_collection = self
.service_proxy
.create_collection(properties, alias)
.await?;
let collection_path: ObjectPath = {
let created_path = created_collection.collection;
if created_path.as_str() == "/" {
let prompt_path = created_collection.prompt;
let prompt_res = exec_prompt(self.conn.clone(), &prompt_path).await?;
prompt_res.try_into()?
} else {
created_path.into()
}
};
Collection::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
collection_path.into(),
)
.await
}
pub async fn search_items(
&self,
attributes: HashMap<&str, &str>,
) -> Result<SearchItemsResult<Item<'_>>, Error> {
let items = self.service_proxy.search_items(attributes).await?;
let object_paths_to_items = |items: Vec<_>| {
futures_util::future::join_all(items.into_iter().map(|item_path| {
Item::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
item_path,
)
}))
};
Ok(SearchItemsResult {
unlocked: object_paths_to_items(items.unlocked)
.await
.into_iter()
.collect::<Result<_, _>>()?,
locked: object_paths_to_items(items.locked)
.await
.into_iter()
.collect::<Result<_, _>>()?,
})
}
pub async fn unlock_all(&self, items: &[&Item<'_>]) -> Result<(), Error> {
let objects = items.iter().map(|i| &*i.item_path).collect();
let lock_action_res = self.service_proxy.unlock(objects).await?;
if lock_action_res.object_paths.is_empty() {
exec_prompt(self.conn.clone(), &lock_action_res.prompt).await?;
}
Ok(())
}
pub async fn get_item_by_path(&'a self, item_path: OwnedObjectPath) -> Result<Item<'a>, Error> {
Item::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
item_path,
)
.await
}
pub async fn get_collection_by_path(
&'a self,
collection_path: OwnedObjectPath,
) -> Result<Collection<'a>, Error> {
Collection::new(
self.conn.clone(),
&self.session,
&self.service_proxy,
collection_path,
)
.await
}
}
#[cfg(test)]
mod test {
use super::*;
use std::convert::TryFrom;
use zbus::zvariant::ObjectPath;
#[tokio::test]
async fn should_create_secret_service() {
SecretService::connect(EncryptionType::Plain).await.unwrap();
}
#[tokio::test]
async fn should_get_all_collections() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
let collections = ss.get_all_collections().await.unwrap();
assert!(!collections.is_empty(), "no collections found");
}
#[tokio::test]
async fn should_get_collection_by_alias() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
ss.get_collection_by_alias("session").await.unwrap();
}
#[tokio::test]
async fn should_return_error_if_collection_doesnt_exist() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
match ss
.get_collection_by_alias("definitely_defintely_does_not_exist")
.await
{
Err(Error::NoResult) => {}
_ => panic!(),
};
}
#[tokio::test]
async fn should_get_default_collection() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
ss.get_default_collection().await.unwrap();
}
#[tokio::test]
async fn should_get_any_collection() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
let _ = ss.get_any_collection().await.unwrap();
}
#[test_with::no_env(GITHUB_ACTIONS)]
#[tokio::test]
async fn should_create_and_delete_collection() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
let test_collection = ss.create_collection("Test", "").await.unwrap();
assert_eq!(
ObjectPath::from(test_collection.collection_path.clone()),
ObjectPath::try_from("/org/freedesktop/secrets/collection/Test").unwrap()
);
test_collection.delete().await.unwrap();
}
#[tokio::test]
async fn should_search_items() {
let ss = SecretService::connect(EncryptionType::Plain).await.unwrap();
let collection = ss.get_default_collection().await.unwrap();
let item = collection
.create_item(
"test",
HashMap::from([("test_attribute_in_ss", "test_value")]),
b"test_secret",
false,
"text/plain",
)
.await
.unwrap();
ss.search_items(HashMap::new()).await.unwrap();
let bad_search = ss
.search_items(HashMap::from([("test", "test")]))
.await
.unwrap();
assert_eq!(bad_search.unlocked.len(), 0);
assert_eq!(bad_search.locked.len(), 0);
let search_item = ss
.search_items(HashMap::from([("test_attribute_in_ss", "test_value")]))
.await
.unwrap();
assert_eq!(item.item_path, search_item.unlocked[0].item_path);
assert_eq!(search_item.locked.len(), 0);
item.delete().await.unwrap();
}
}