modality_auth_token/
lib.rs1#![deny(warnings, clippy::all)]
2use 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 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 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#[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 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 }
220 }
221 });
222 }
223}