auxon_sdk/auth_token/
mod.rs

1//! Library relating to the handling of modality's auth tokens:
2//!
3//! * Representation in memory
4//! * Stringy-hexy serialization
5//! * A tiny file format that pairs an auth token with a plaintext user name
6use hex::FromHexError;
7use std::{
8    env,
9    path::{Path, PathBuf},
10    str::FromStr,
11};
12use thiserror::Error;
13use token_user_file::{
14    read_user_auth_token_file, TokenUserFileReadError, USER_AUTH_TOKEN_FILE_NAME,
15};
16
17pub mod token_user_file;
18
19pub const MODALITY_AUTH_TOKEN_ENV_VAR: &str = "MODALITY_AUTH_TOKEN";
20
21const DEFAULT_CONTEXT_DIR: &str = "modality_cli";
22const MODALITY_CONTEXT_DIR_ENV_VAR: &str = "MODALITY_CONTEXT_DIR";
23
24#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
25#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
26#[repr(transparent)]
27pub struct AuthToken(Vec<u8>);
28
29impl AuthToken {
30    /// Load an auth token meant for user-api usage
31    pub fn load() -> Result<Self, LoadAuthTokenError> {
32        if let Ok(s) = std::env::var(MODALITY_AUTH_TOKEN_ENV_VAR) {
33            return Ok(AuthTokenHexString(s).try_into()?);
34        }
35
36        let context_dir = Self::context_dir()?;
37        let user_auth_token_path = context_dir.join(USER_AUTH_TOKEN_FILE_NAME);
38        if user_auth_token_path.exists() {
39            if let Some(file_contents) = read_user_auth_token_file(&user_auth_token_path)? {
40                return Ok(file_contents.auth_token);
41            } else {
42                return Err(LoadAuthTokenError::NoTokenInFile(
43                    user_auth_token_path.to_owned(),
44                ));
45            }
46        }
47
48        Err(LoadAuthTokenError::NoAuthToken)
49    }
50
51    fn context_dir() -> Result<PathBuf, LoadAuthTokenError> {
52        match env::var(MODALITY_CONTEXT_DIR_ENV_VAR) {
53            Ok(val) => Ok(PathBuf::from(val)),
54            Err(env::VarError::NotUnicode(_)) => {
55                Err(LoadAuthTokenError::EnvVarSpecifiedModalityContextDirNonUtf8)
56            }
57            Err(env::VarError::NotPresent) => {
58                let config_dir = if cfg!(windows) {
59                    // Attempt to use APPDATA env var on windows, it's the same as the
60                    // underlying winapi call within config_dir but in env var form rather
61                    // than a winapi call, it's not available on all versions like xp, apparently
62                    if let Ok(val) = env::var("APPDATA") {
63                        let dir = Path::new(&val);
64                        dir.to_path_buf()
65                    } else {
66                        dirs::config_dir().ok_or(LoadAuthTokenError::ContextDir)?
67                    }
68                } else {
69                    dirs::config_dir().ok_or(LoadAuthTokenError::ContextDir)?
70                };
71                Ok(config_dir.join(DEFAULT_CONTEXT_DIR))
72            }
73        }
74    }
75}
76
77#[derive(Debug, Error)]
78pub enum LoadAuthTokenError {
79    #[error(transparent)]
80    AuthTokenStringDeserializationError(#[from] AuthTokenStringDeserializationError),
81
82    #[error(transparent)]
83    TokenUserFileReadError(#[from] TokenUserFileReadError),
84
85    #[error("Auth token not found in token file at {0}")]
86    NoTokenInFile(PathBuf),
87
88    #[error(
89        "The MODALITY_CONTEXT_DIR environment variable contained a non-UTF-8-compatible string"
90    )]
91    EnvVarSpecifiedModalityContextDirNonUtf8,
92
93    #[error("Could not determine the user context configuration directory")]
94    ContextDir,
95
96    #[error("Cannot resolve config dir")]
97    NoConfigDir,
98
99    #[error("Couldn't find an auth token to load.")]
100    NoAuthToken,
101}
102
103impl From<Vec<u8>> for AuthToken {
104    fn from(v: Vec<u8>) -> Self {
105        AuthToken(v)
106    }
107}
108
109impl From<AuthToken> for Vec<u8> {
110    fn from(v: AuthToken) -> Self {
111        v.0
112    }
113}
114
115impl AsRef<[u8]> for AuthToken {
116    fn as_ref(&self) -> &[u8] {
117        &self.0
118    }
119}
120
121/// A possibly-human-readable UTF8 encoding of an auth token
122/// as a series of lowercase case character pairs.
123#[derive(
124    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
125)]
126#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
127#[repr(transparent)]
128pub struct AuthTokenHexString(String);
129
130impl std::fmt::Display for AuthTokenHexString {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(f, "{}", self.0)
133    }
134}
135
136impl FromStr for AuthTokenHexString {
137    type Err = AuthTokenStringDeserializationError;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        decode_auth_token_hex_str(s)
141    }
142}
143
144impl AuthTokenHexString {
145    pub fn as_str(&self) -> &str {
146        self.0.as_str()
147    }
148}
149
150impl From<AuthTokenHexString> for String {
151    fn from(v: AuthTokenHexString) -> Self {
152        v.0
153    }
154}
155
156impl From<AuthToken> for AuthTokenHexString {
157    fn from(v: AuthToken) -> Self {
158        AuthTokenHexString(hex::encode(v.0))
159    }
160}
161
162impl TryFrom<AuthTokenHexString> for AuthToken {
163    type Error = AuthTokenStringDeserializationError;
164
165    fn try_from(v: AuthTokenHexString) -> Result<Self, Self::Error> {
166        decode_auth_token_hex(v.as_str())
167    }
168}
169
170pub fn decode_auth_token_hex(s: &str) -> Result<AuthToken, AuthTokenStringDeserializationError> {
171    hex::decode(s)
172        .map_err(|hex_error|match hex_error {
173            FromHexError::InvalidHexCharacter { .. } => AuthTokenStringDeserializationError::InvalidHexCharacter,
174            FromHexError::OddLength => AuthTokenStringDeserializationError::OddLength,
175            FromHexError::InvalidStringLength => {
176                panic!("An audit of the hex crate showed that the InvalidStringLength error is impossible for the `decode` method call.");
177            }
178        })
179        .map(AuthToken::from)
180}
181
182fn decode_auth_token_hex_str(
183    s: &str,
184) -> Result<AuthTokenHexString, AuthTokenStringDeserializationError> {
185    decode_auth_token_hex(s).map(AuthTokenHexString::from)
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Hash, Error, serde::Serialize, serde::Deserialize)]
189#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
190pub enum AuthTokenStringDeserializationError {
191    #[error("Invalid character in the auth token hex representation. Characters ought to be '0' through '9', 'a' through 'f', or 'A' through 'F'")]
192    InvalidHexCharacter,
193    #[error("Auth token hex representation must contain an even number of hex-digits")]
194    OddLength,
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use proptest::prelude::*;
201
202    #[test]
203    fn decode_auth_token_hex_never_panics() {
204        proptest!(|(s in ".*")| {
205            match decode_auth_token_hex(&s) {
206                Ok(at) => {
207                    // If valid, must be round trippable
208                    let aths = AuthTokenHexString::from(at.clone());
209                    let at_two = AuthToken::try_from(aths).unwrap();
210                    assert_eq!(at, at_two);
211                },
212                Err(AuthTokenStringDeserializationError::OddLength) => {
213                    prop_assert!(s.len() % 2 == 1);
214                }
215                Err(AuthTokenStringDeserializationError::InvalidHexCharacter) => {
216                    // Cool with this error
217                }
218            }
219        });
220    }
221}