auxon_sdk/auth_token/
mod.rs1use 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 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 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#[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 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 }
218 }
219 });
220 }
221}