Skip to content

Commit 09527c7

Browse files
Merge pull request #113 from brotskydotcom/secret-service-always-search-all
This cleans up the secret-service API and docs. Primarily doc changes, but also removed some unnecessary derived traits.
2 parents 981e715 + 530ee89 commit 09527c7

File tree

1 file changed

+102
-45
lines changed

1 file changed

+102
-45
lines changed

src/secret_service.rs

+102-45
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*!
22
3-
# Secret Service credential store
3+
# secret-service credential store
44
5-
Items in tne secret service are identified by an arbitrary collection
5+
Items in the secret-service are identified by an arbitrary collection
66
of attributes, and each has "label" for use in graphical editors. The keyring
77
client uses the following attributes:
88
@@ -18,29 +18,33 @@ will have their items found by our searches. (Items we write with
1818
the same service and user attributes but different target attributes will also come
1919
back in searches, but they are filtered out of the results automatically.)
2020
21-
New credentials are always created with all four attributes, and if they have
21+
New items are always created with all four attributes, and if they have
2222
a non-default target then they are created in a collection whose label matches
2323
the target (creating it if necessary).
2424
25-
The [set_password] implementation always prefers to update the password on an
26-
existing credential, so it will only create a new item if it can't find one.
27-
This allows better compatibility with 3rd party clients, and reduces the chance
25+
Setting the password on an entry will always update the password on an
26+
existing item in preference to creating a new item.
27+
This provides better compatibility with 3rd party clients that may already
28+
have created items that match the entry, and reduces the chance
2829
of ambiguity in later searches.
2930
*/
3031
use std::collections::HashMap;
3132

3233
use secret_service::blocking::{Collection, Item, SecretService};
33-
pub use secret_service::{EncryptionType, Error};
34+
use secret_service::{EncryptionType, Error};
3435

3536
use super::credential::{Credential, CredentialApi, CredentialBuilder, CredentialBuilderApi};
3637
use super::error::{decode_password, Error as ErrorCode, Result};
3738

39+
/// The representation of an item in the secret-service.
40+
///
3841
/// This structure has two roles. On the one hand, it captures all the
39-
/// information user specify for an [Entry] and so is the basis for our search
40-
/// (or creation) of items that match that entry. On the other hand, when
41-
/// a search is ambiguous, each item found is represented by a structure that
42+
/// information a user specifies for an [Entry](crate::Entry)
43+
/// and so is the basis for our search
44+
/// (or creation) of an item for that entry. On the other hand, when
45+
/// a search is ambiguous, each item found is represented by a credential that
4246
/// has the same attributes and label as the item.
43-
#[derive(Debug, Clone, PartialEq, Eq)]
47+
#[derive(Debug, Clone)]
4448
pub struct SsCredential {
4549
pub attributes: HashMap<String, String>,
4650
pub label: String,
@@ -49,9 +53,13 @@ pub struct SsCredential {
4953

5054
impl CredentialApi for SsCredential {
5155
/// Sets the password on a unique matching item, if it exists, or creates one if necessary.
52-
/// When creating, the item is put into a collection named by the credential's `target`,
53-
/// which must be explicitly specified. If there is more than one matching item,
54-
/// returns an [ErrorCode::Ambiguous] error with a credential for each one.
56+
///
57+
/// If there are multiple matches,
58+
/// returns an [Ambiguous](ErrorCode::Ambiguous) error with a credential for each
59+
/// matching item.
60+
///
61+
/// When creating, the item is put into a collection named by the credential's `target`
62+
/// attribute.
5563
fn set_password(&self, password: &str) -> Result<()> {
5664
let ss = SecretService::connect(EncryptionType::Dh).map_err(platform_failure)?;
5765
// first try to find a unique, existing, matching item and set its password
@@ -78,17 +86,25 @@ impl CredentialApi for SsCredential {
7886
Ok(())
7987
}
8088

81-
/// Gets the password on a unique matching item, if it exists. If there are no
82-
/// matching items, returns an [ErrorCode::NoEntry] error. If there is more than
83-
/// one matching item, returns an [ErrorCode::Ambiguous] error with a credential for each one.
89+
/// Gets the password on a unique matching item, if it exists.
90+
///
91+
/// If there are no
92+
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
93+
/// If there are multiple matches,
94+
/// returns an [Ambiguous](ErrorCode::Ambiguous)
95+
/// error with a credential for each matching item.
8496
fn get_password(&self) -> Result<String> {
8597
let passwords: Vec<String> = self.map_matching_items(get_item_password, true)?;
8698
Ok(passwords[0].clone())
8799
}
88100

89-
/// Deletes the unique matching item, if it exists. If there are no
90-
/// matching items, returns an [ErrorCode::NoEntry] error. If there is more than
91-
/// one matching item, returns an [ErrorCode::Ambiguous] error with a credential for each one.
101+
/// Deletes the unique matching item, if it exists.
102+
///
103+
/// If there are no
104+
/// matching items, returns a [NoEntry](ErrorCode::NoEntry) error.
105+
/// If there are multiple matches,
106+
/// returns an [Ambiguous](ErrorCode::Ambiguous)
107+
/// error with a credential for each matching item.
92108
fn delete_password(&self) -> Result<()> {
93109
self.map_matching_items(delete_item, true)?;
94110
Ok(())
@@ -104,9 +120,12 @@ impl CredentialApi for SsCredential {
104120
impl SsCredential {
105121
/// Create a credential for the given target, service, and user.
106122
///
107-
/// Creating this credential does not create an Item in the secret service
108-
/// until this credential is wrapped in an [Entry] and [set_password] is
109-
/// called on that entry.
123+
/// The target defaults to `default` (the default secret-service collection).
124+
///
125+
/// Creating this credential does not create a matching item.
126+
/// If there isn't already one there, it will be created only
127+
/// when [set_password](SsCredential::set_password) is
128+
/// called.
110129
pub fn new_with_target(target: Option<&str>, service: &str, user: &str) -> Result<Self> {
111130
if let Some("") = target {
112131
return Err(empty_target());
@@ -128,12 +147,10 @@ impl SsCredential {
128147
})
129148
}
130149

131-
/// Create a credential from an underlying Item.
150+
/// Create a credential from an underlying item.
132151
///
133152
/// The created credential will have all the attributes and label
134-
/// of the underlying item, so you can examine them. But it won't
135-
/// have a target (and so can't be used to create new a new item)
136-
/// unless the underlying item has a `target` attribute.
153+
/// of the underlying item, so you can examine them.
137154
pub fn new_from_item(item: &Item) -> Result<Self> {
138155
let attributes = item.get_attributes().map_err(decode_error)?;
139156
let target = attributes.get("target").cloned();
@@ -144,31 +161,39 @@ impl SsCredential {
144161
})
145162
}
146163

147-
/// Construct a credential from the underlying matching Item, if there is exactly one.
164+
/// Construct a credential for this credential's underlying matching item,
165+
/// if there is exactly one.
148166
pub fn new_from_matching_item(&self) -> Result<Self> {
149167
let credentials = self.map_matching_items(Self::new_from_item, true)?;
150168
Ok(credentials[0].clone())
151169
}
152170

153-
/// If there are multiple matching Items, get all of their passwords.
154-
/// (This is useful if [get_password] returns an `Ambiguous` error.)
171+
/// If there are multiple matching items for this credential, get all of their passwords.
172+
///
173+
/// (This is useful if [get_password](SsCredential::get_password)
174+
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
155175
pub fn get_all_passwords(&self) -> Result<Vec<String>> {
156176
self.map_matching_items(get_item_password, true)
157177
}
158178

159-
/// If there are multiple matching Items, delete all of them.
160-
/// (This is useful if [delete_password] returns an `Ambiguous` error.)
179+
/// If there are multiple matching items for this credential, delete all of them.
180+
///
181+
/// (This is useful if [delete_password](SsCredential::delete_password)
182+
/// returns an [Ambiguous](ErrorCode::Ambiguous) error.)
161183
pub fn delete_all_passwords(&self) -> Result<()> {
162184
self.map_matching_items(delete_item, true)?;
163185
Ok(())
164186
}
165187

166188
/// Map a function over all of the items matching this credential.
189+
///
167190
/// Items are unlocked before the function is applied.
191+
///
168192
/// If `require_unique` is true, and there are no matching items, then
169-
/// [ErrorCode::NoEntry] is returned.
170-
/// If `require_unique` is true, and there is more than one matching item,
171-
/// then[ErrorCode::Ambiguous] is returned with a vector containing one
193+
/// a [NoEntry](ErrorCode::NoEntry) error is returned.
194+
/// If `require_unique` is true, and there are multiple matches,
195+
/// then an [Ambiguous](ErrorCode::Ambiguous) error is returned
196+
/// with a vector containing one
172197
/// credential for each of the matching items.
173198
pub fn map_matching_items<F, T>(&self, f: F, require_unique: bool) -> Result<Vec<T>>
174199
where
@@ -179,8 +204,8 @@ impl SsCredential {
179204
let attributes: HashMap<&str, &str> = self.search_attributes().into_iter().collect();
180205
let search = ss.search_items(attributes).map_err(decode_error)?;
181206
let target = self.target.as_ref().ok_or_else(empty_target)?;
182-
let unlocked = matching_items(&search.unlocked, target)?;
183-
let locked = matching_items(&search.locked, target)?;
207+
let unlocked = matching_target_items(&search.unlocked, target)?;
208+
let locked = matching_target_items(&search.locked, target)?;
184209
if require_unique {
185210
let count = locked.len() + unlocked.len();
186211
if count == 0 {
@@ -216,7 +241,8 @@ impl SsCredential {
216241
.collect()
217242
}
218243

219-
/// Similar to all_attributes, but this just selects the ones we search on
244+
/// Similar to [all_attributes](SsCredential::all_attributes),
245+
/// but this just selects the ones we search on
220246
fn search_attributes(&self) -> HashMap<&str, &str> {
221247
let mut result: HashMap<&str, &str> = HashMap::new();
222248
result.insert("service", self.attributes["service"].as_str());
@@ -225,21 +251,28 @@ impl SsCredential {
225251
}
226252
}
227253

254+
/// The builder for secret-service credentials
228255
#[derive(Debug, Default)]
229256
pub struct SsCredentialBuilder {}
230257

231-
/// Called by the crate when Secret Service is the default credential store.
258+
/// Returns an instance of the secret-service credential builder.
259+
///
260+
/// If secret-service is the default credential store,
261+
/// this is called once when an entry is first created.
232262
pub fn default_credential_builder() -> Box<CredentialBuilder> {
233263
Box::new(SsCredentialBuilder {})
234264
}
235265

236266
impl CredentialBuilderApi for SsCredentialBuilder {
267+
/// Build an [SsCredential] for the given target, service, and user.
237268
fn build(&self, target: Option<&str>, service: &str, user: &str) -> Result<Box<Credential>> {
238269
Ok(Box::new(SsCredential::new_with_target(
239270
target, service, user,
240271
)?))
241272
}
242273

274+
/// Return the underlying builder object with an `Any` type so that it can
275+
/// be downgraded to an [SsCredentialBuilder] for platform-specific processing.
243276
fn as_any(&self) -> &dyn std::any::Any {
244277
self
245278
}
@@ -248,10 +281,14 @@ impl CredentialBuilderApi for SsCredentialBuilder {
248281
//
249282
// Secret Service utilities
250283
//
251-
/// Find the secret service collection that should contain this item
284+
285+
/// Find the secret service collection whose label is the given name.
286+
///
287+
/// The name `default` is treated specially and is interpreted as naming
288+
/// the default collection regardless of its label (which might be different).
252289
pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collection<'a>> {
253290
let collection = if name.eq("default") {
254-
ss.get_collection_by_alias(name).map_err(decode_error)?
291+
ss.get_default_collection().map_err(decode_error)?
255292
} else {
256293
let all = ss.get_all_collections().map_err(decode_error)?;
257294
let found = all
@@ -265,27 +302,46 @@ pub fn get_collection<'a>(ss: &'a SecretService, name: &str) -> Result<Collectio
265302
Ok(collection)
266303
}
267304

268-
/// Create the secret service collection that will contain this credential
305+
/// Create a secret service collection labeled with the given name.
306+
///
307+
/// If a collection with that name already exists, it is returned.
308+
///
309+
/// The name `default` is specially interpreted to mean the default collection,
310+
/// which always exists.
269311
pub fn create_collection<'a>(ss: &'a SecretService<'a>, name: &str) -> Result<Collection<'a>> {
270-
let collection = ss.create_collection(name, "").map_err(decode_error)?;
312+
let collection = if name.eq("default") {
313+
ss.get_default_collection().map_err(decode_error)?
314+
} else {
315+
ss.create_collection(name, "").map_err(decode_error)?
316+
};
271317
Ok(collection)
272318
}
273319

320+
/// Given an existing item, set its password.
274321
pub fn set_item_password(item: &Item, password: &str) -> Result<()> {
275322
item.set_secret(password.as_bytes(), "text/plain")
276323
.map_err(decode_error)
277324
}
278325

326+
/// Given an existing item, retrieve and decode its password.
279327
pub fn get_item_password(item: &Item) -> Result<String> {
280328
let bytes = item.get_secret().map_err(decode_error)?;
281329
decode_password(bytes)
282330
}
283331

332+
/// Given an existing item, delete it.
284333
pub fn delete_item(item: &Item) -> Result<()> {
285334
item.delete().map_err(decode_error)
286335
}
287336

288-
pub fn matching_items<'a>(source: &'a [Item<'a>], target: &str) -> Result<Vec<&'a Item<'a>>> {
337+
/// Given a slice of items, filter out the ones that have an explicit target
338+
/// attribute that doesn't match the given target.
339+
///
340+
/// References to the matching items are returned in a new vector.
341+
pub fn matching_target_items<'a>(
342+
source: &'a [Item<'a>],
343+
target: &str,
344+
) -> Result<Vec<&'a Item<'a>>> {
289345
let mut result: Vec<&'a Item<'a>> = vec![];
290346
for i in source.iter() {
291347
match i.get_attributes().map_err(decode_error)?.get("target") {
@@ -300,6 +356,7 @@ pub fn matching_items<'a>(source: &'a [Item<'a>], target: &str) -> Result<Vec<&'
300356
//
301357
// Error utilities
302358
//
359+
303360
/// Map underlying secret-service errors to crate errors with
304361
/// appropriate annotation.
305362
pub fn decode_error(err: Error) -> ErrorCode {
@@ -411,7 +468,7 @@ mod tests {
411468

412469
fn probe_collection(name: &str) -> bool {
413470
use secret_service::blocking::SecretService;
414-
pub use secret_service::EncryptionType;
471+
use secret_service::EncryptionType;
415472

416473
let ss =
417474
SecretService::connect(EncryptionType::Dh).expect("Can't connect to secret service");

0 commit comments

Comments
 (0)