1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#![deny(warnings, clippy::all)]
//! Library relating to the handling of modality's auth tokens:
//!
//! * Representation in memory
//! * Stringy-hexy serialization
//! * A tiny file format that pairs an auth token with a plaintext user name
use hex::FromHexError;
use std::{
    env,
    path::{Path, PathBuf},
    str::FromStr,
};
use thiserror::Error;
use token_user_file::{
    read_user_auth_token_file, TokenUserFileReadError, USER_AUTH_TOKEN_FILE_NAME,
};

pub mod token_user_file;

pub const MODALITY_AUTH_TOKEN_ENV_VAR: &str = "MODALITY_AUTH_TOKEN";

const DEFAULT_CONTEXT_DIR: &str = "modality_cli";
const MODALITY_CONTEXT_DIR_ENV_VAR: &str = "MODALITY_CONTEXT_DIR";

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct AuthToken(Vec<u8>);

impl AuthToken {
    /// Load an auth token meant for user-api usage
    pub fn load() -> Result<Self, LoadAuthTokenError> {
        if let Ok(s) = std::env::var(MODALITY_AUTH_TOKEN_ENV_VAR) {
            return Ok(AuthTokenHexString(s).try_into()?);
        }

        let context_dir = Self::context_dir()?;
        let user_auth_token_path = context_dir.join(USER_AUTH_TOKEN_FILE_NAME);
        if user_auth_token_path.exists() {
            if let Some(file_contents) = read_user_auth_token_file(&user_auth_token_path)? {
                return Ok(file_contents.auth_token);
            } else {
                return Err(LoadAuthTokenError::NoTokenInFile(
                    user_auth_token_path.to_owned(),
                ));
            }
        }

        Err(LoadAuthTokenError::NoAuthToken)
    }

    fn context_dir() -> Result<PathBuf, LoadAuthTokenError> {
        match env::var(MODALITY_CONTEXT_DIR_ENV_VAR) {
            Ok(val) => Ok(PathBuf::from(val)),
            Err(env::VarError::NotUnicode(_)) => {
                Err(LoadAuthTokenError::EnvVarSpecifiedModalityContextDirNonUtf8)
            }
            Err(env::VarError::NotPresent) => {
                let config_dir = if cfg!(windows) {
                    // Attempt to use APPDATA env var on windows, it's the same as the
                    // underlying winapi call within config_dir but in env var form rather
                    // than a winapi call, it's not available on all versions like xp, apparently
                    if let Ok(val) = env::var("APPDATA") {
                        let dir = Path::new(&val);
                        dir.to_path_buf()
                    } else {
                        dirs::config_dir().ok_or(LoadAuthTokenError::ContextDir)?
                    }
                } else {
                    dirs::config_dir().ok_or(LoadAuthTokenError::ContextDir)?
                };
                Ok(config_dir.join(DEFAULT_CONTEXT_DIR))
            }
        }
    }
}

#[derive(Debug, Error)]
pub enum LoadAuthTokenError {
    #[error(transparent)]
    AuthTokenStringDeserializationError(#[from] AuthTokenStringDeserializationError),

    #[error(transparent)]
    TokenUserFileReadError(#[from] TokenUserFileReadError),

    #[error("Auth token not found in token file at {0}")]
    NoTokenInFile(PathBuf),

    #[error(
        "The MODALITY_CONTEXT_DIR environment variable contained a non-UTF-8-compatible string"
    )]
    EnvVarSpecifiedModalityContextDirNonUtf8,

    #[error("Could not determine the user context configuration directory")]
    ContextDir,

    #[error("Cannot resolve config dir")]
    NoConfigDir,

    #[error("Couldn't find an auth token to load.")]
    NoAuthToken,
}

impl From<Vec<u8>> for AuthToken {
    fn from(v: Vec<u8>) -> Self {
        AuthToken(v)
    }
}

impl From<AuthToken> for Vec<u8> {
    fn from(v: AuthToken) -> Self {
        v.0
    }
}

impl AsRef<[u8]> for AuthToken {
    fn as_ref(&self) -> &[u8] {
        &self.0
    }
}

/// A possibly-human-readable UTF8 encoding of an auth token
/// as a series of lowercase case character pairs.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct AuthTokenHexString(String);

impl std::fmt::Display for AuthTokenHexString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for AuthTokenHexString {
    type Err = AuthTokenStringDeserializationError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        decode_auth_token_hex_str(s)
    }
}

impl AuthTokenHexString {
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

impl From<AuthTokenHexString> for String {
    fn from(v: AuthTokenHexString) -> Self {
        v.0
    }
}

impl From<AuthToken> for AuthTokenHexString {
    fn from(v: AuthToken) -> Self {
        AuthTokenHexString(hex::encode(v.0))
    }
}

impl TryFrom<AuthTokenHexString> for AuthToken {
    type Error = AuthTokenStringDeserializationError;

    fn try_from(v: AuthTokenHexString) -> Result<Self, Self::Error> {
        decode_auth_token_hex(v.as_str())
    }
}

pub fn decode_auth_token_hex(s: &str) -> Result<AuthToken, AuthTokenStringDeserializationError> {
    hex::decode(s)
        .map_err(|hex_error|match hex_error {
            FromHexError::InvalidHexCharacter { .. } => AuthTokenStringDeserializationError::InvalidHexCharacter,
            FromHexError::OddLength => AuthTokenStringDeserializationError::OddLength,
            FromHexError::InvalidStringLength => {
                panic!("An audit of the hex crate showed that the InvalidStringLength error is impossible for the `decode` method call.");
            }
        })
        .map(AuthToken::from)
}

fn decode_auth_token_hex_str(
    s: &str,
) -> Result<AuthTokenHexString, AuthTokenStringDeserializationError> {
    decode_auth_token_hex(s).map(AuthTokenHexString::from)
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AuthTokenStringDeserializationError {
    #[error("Invalid character in the auth token hex representation. Characters ought to be '0' through '9', 'a' through 'f', or 'A' through 'F'")]
    InvalidHexCharacter,
    #[error("Auth token hex representation must contain an even number of hex-digits")]
    OddLength,
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    #[test]
    fn decode_auth_token_hex_never_panics() {
        proptest!(|(s in ".*")| {
            match decode_auth_token_hex(&s) {
                Ok(at) => {
                    // If valid, must be round trippable
                    let aths = AuthTokenHexString::from(at.clone());
                    let at_two = AuthToken::try_from(aths).unwrap();
                    assert_eq!(at, at_two);
                },
                Err(AuthTokenStringDeserializationError::OddLength) => {
                    prop_assert!(s.len() % 2 == 1);
                }
                Err(AuthTokenStringDeserializationError::InvalidHexCharacter) => {
                    // Cool with this error
                }
            }
        });
    }
}