1use crate::api::ApiError;
2
3const SERVICE: &str = "jira-cli";
4
5pub fn store(profile: &str, token: &str) -> Result<(), ApiError> {
6 entry(profile)?
7 .set_password(token)
8 .map_err(|error| keyring_error("store", error))
9}
10
11pub fn load(profile: &str) -> Result<String, ApiError> {
12 entry(profile)?
13 .get_password()
14 .map_err(|error| keyring_error("read", error))
15}
16
17pub fn load_optional(profile: &str) -> Result<Option<String>, ApiError> {
18 match entry(profile)?.get_password() {
19 Ok(token) => Ok(Some(token)),
20 Err(keyring::Error::NoEntry) => Ok(None),
21 Err(error) => Err(keyring_error("read", error)),
22 }
23}
24
25pub fn delete(profile: &str) -> Result<bool, ApiError> {
26 match entry(profile)?.delete_credential() {
27 Ok(()) => Ok(true),
28 Err(keyring::Error::NoEntry) => Ok(false),
29 Err(error) => Err(keyring_error("delete", error)),
30 }
31}
32
33pub fn available() -> Result<(), ApiError> {
34 keyring::Entry::store_status()
35 .as_ref()
36 .map_err(|error| unavailable_error(&error.to_string()))
37 .map(|_| ())
38}
39
40fn entry(profile: &str) -> Result<keyring::Entry, ApiError> {
41 keyring::Entry::new(SERVICE, &format!("profile:{profile}"))
42 .map_err(|error| keyring_error("open", error))
43}
44
45fn unavailable_error(detail: &str) -> ApiError {
46 if detail.contains("ServiceUnknown")
47 && (detail.contains("org.freedesktop.secrets") || detail.contains("Secret Service"))
48 {
49 ApiError::InvalidInput(
50 "OS credential store is unavailable: no Secret Service provider is running".into(),
51 )
52 } else {
53 ApiError::InvalidInput(format!("OS credential store is unavailable: {detail}"))
54 }
55}
56
57fn keyring_error(operation: &str, error: keyring::Error) -> ApiError {
58 let message = match error {
59 keyring::Error::NoEntry => {
60 "credential not found for profile; run `jira auth login` or `jira auth migrate`"
61 .to_string()
62 }
63 other => format!("failed to {operation} OS-keychain credential: {other}"),
64 };
65 ApiError::InvalidInput(message)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn missing_linux_secret_service_has_a_human_error() {
74 let error = unavailable_error(
75 "Platform failure: org.freedesktop.DBus.Error.ServiceUnknown: the name org.freedesktop.secrets was not provided",
76 );
77 assert!(error.to_string().contains("no Secret Service provider"));
78 }
79}