Skip to main content

jira_cli/
credentials.rs

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}