modality_auth_token/
lib.rs

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