ddmw_client/
auth.rs

1//! Authentication and unauthentication.
2
3use std::borrow::Borrow;
4use std::fs::File;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use tokio::io::{AsyncRead, AsyncWrite};
9
10use tokio_util::codec::Framed;
11
12use serde::Deserialize;
13
14use blather::Telegram;
15
16use crate::utils;
17use crate::Error;
18
19
20#[derive(Debug, Default)]
21pub struct Builder {
22  name: Option<String>,
23  pass_file: Option<String>,
24  pass: Option<String>,
25  token_file: Option<String>,
26  token: Option<String>
27}
28
29impl Builder {
30  pub fn new() -> Self {
31    Self::default()
32  }
33
34  pub fn name<N>(mut self, nm: N) -> Self
35  where
36    N: ToString
37  {
38    self.name = Some(nm.to_string());
39    self
40  }
41
42  pub fn pass_file<P>(mut self, p: P) -> Self
43  where
44    P: ToString
45  {
46    self.pass_file = Some(p.to_string());
47    self
48  }
49
50  pub fn pass<P>(mut self, p: P) -> Self
51  where
52    P: ToString
53  {
54    self.pass = Some(p.to_string());
55    self
56  }
57
58  pub fn token_file<T>(mut self, t: T) -> Self
59  where
60    T: ToString
61  {
62    self.token_file = Some(t.to_string());
63    self
64  }
65
66  pub fn token<T>(mut self, t: T) -> Self
67  where
68    T: ToString
69  {
70    self.token = Some(t.to_string());
71    self
72  }
73
74  /// Construct an [`Auth`] buffer.
75  pub fn build(self) -> Result<Auth, Error> {
76    // ToDo: Validate parameters
77    Ok(Auth {
78      name: self.name,
79      pass_file: self.pass_file,
80      pass: self.pass,
81      token_file: self.token_file,
82      token: self.token
83    })
84  }
85}
86
87
88/// Authentication context used to signal how to authenticate a connection.
89#[derive(Clone, Debug, Default, Deserialize)]
90pub struct Auth {
91  /// Account name used to authenticate.
92  pub name: Option<String>,
93
94  /// Load raw account passphrase from the specified filename.
95  #[serde(rename = "pass-file")]
96  pub pass_file: Option<String>,
97
98  /// Raw account passphrase to authenticate with.  Only used if `name` has
99  /// been set.
100  pub pass: Option<String>,
101
102  /// Use the specified file for authentication token storage.
103  #[serde(rename = "token-file")]
104  pub token_file: Option<String>,
105
106  /// Authentication token.
107  pub token: Option<String>
108}
109
110
111impl Auth {
112  /// Return `true` if there's either a raw passphrase set in `pass` or a
113  /// passphrase file has beeen set in `pass_file`.  This function does not
114  /// validate if the passphrase file exists or is accessible.
115  pub fn have_pass(&self) -> bool {
116    self.pass.is_some() || self.pass_file.is_some()
117  }
118
119  /// Get passphrase.
120  ///
121  /// Return the raw `pass` field if set.  Otherwise, load the `pass_file` if
122  /// set, and return error if the passphrase could not be loaded from the
123  /// file.
124  ///
125  /// If neither `pass` nor `pass_file` have been set, return an error.
126  pub fn get_pass(&self) -> Result<String, Error> {
127    // 1. Return raw passphrase if set
128    // 2. Load raw passphrase from file, if set.  Return error if file could
129    //    not be read.
130    // 3. Return `Ok(None)`
131    if let Some(pass) = &self.pass {
132      Ok(pass.clone())
133    } else if let Some(fname) = &self.pass_file {
134      if let Some(pass) = utils::read_single_line(fname) {
135        Ok(pass)
136      } else {
137        return Err(Error::invalid_cred(
138          "Unable to read passphrase from file"
139        ));
140      }
141    } else {
142      Err(Error::invalid_cred("Missing passphrase"))
143    }
144  }
145
146  /// Get authentication token.
147  ///
148  /// Return the raw `token` field if set.  Otherwise, check if `token_file` is
149  /// set.  If it is, then attempt to load the token from it.  If the file
150  /// _does not_ exist, then:
151  /// - Return `Ok(None)` if account name and pass(file) have been set.
152  /// - Return error if account name has not been set.
153  pub fn get_token(&self) -> Result<Option<String>, Error> {
154    // 1. Return raw token if set.
155    // 2. If a token file has been specified, then:
156    //    - If file exists, then read token from it.
157    //    - If file does not exist, then:
158    //      - If username is set and pass(file) is set, then return `Ok(None)`,
159    //        assuming the caller wants to request a token and store it in the
160    //        specified file.
161    //      - If neither username nor pass(file) is set, then return an error,
162    //        since the token file is missing.
163    if let Some(tkn) = &self.token {
164      Ok(Some(tkn.clone()))
165    } else if let Some(fname) = &self.token_file {
166      let fname = Path::new(&fname);
167      if fname.exists() {
168        // Token file exists, attempt to load token from it
169        if let Some(tkn) = utils::read_single_line(fname) {
170          // Got it, hopefully.  The server will validate it.
171          Ok(Some(tkn))
172        } else {
173          // Unable to read file.
174          Err(Error::invalid_cred("Unable to read token from file"))
175        }
176      } else if self.name.is_none() {
177        // Missing account name, so clearly the authentication call won't be
178        // able to request a token to be stored in the non-existent file.
179        Err(Error::invalid_cred("Unable to read token from file"))
180      } else if self.pass.is_none() && self.pass_file.is_none() {
181        // Have account name, but no passphrase, so authentication can't
182        // succeed.
183        Err(Error::invalid_cred("Missing passphrase for token request"))
184      } else {
185        // Token file does not exist, but an account name and pass(file) was
186        // specified, so assume the caller will request an authentication token
187        // and store to the specified location.
188        Ok(None)
189      }
190    } else {
191      // Neither token nor token file was specified
192      Ok(None)
193    }
194  }
195
196
197  /// Helper function for authenticating a connection.
198  ///
199  /// Authenticates the connection specified in `conn`, using the credentials
200  /// stored in the `Auth` buffer using the following logic:
201  ///
202  /// 1. If a raw token has been supplied in the `token` field, then attempt to
203  ///    authenticate with it and return the results.
204  /// 2. If a `token_file` has been been set, then:
205  ///    - If the file exists, try to load the authentication token,
206  ///      authenticate with it, and return the results.
207  ///    - If the file does not exist, then:
208  ///      - If account name and/or passphrase have not been set, then return
209  ///        error.
210  ///      - If account name and passphrase have been set, then continue.
211  /// 3. Make sure that an account name and a passphrase has been set. The
212  ///    passphrase is either set from the `pass` field or by loading the
213  ///    contents of the file in `pass_file`.  Return error account name or
214  ///    passphrase can not be acquired.
215  /// 4. Authenticate using account name and passphrase.  If a `token_file` was
216  ///    specified, then request an authentication token and store it in
217  ///    `token_file` on success.  Return error on failure.
218  pub async fn authenticate<C>(
219    &self,
220    conn: &mut Framed<C, blather::Codec>
221  ) -> Result<Option<String>, Error>
222  where
223    C: AsyncRead + AsyncWrite + Unpin
224  {
225    // Attempt to get token.
226    // This will return Ok(None) if there's no token to be added to the `Auth`
227    // server request, but it can also mean that the caller wants a token to be
228    // *requested* (the `token_file` field needs to be checked for this).
229    let tkn = self.get_token()?;
230
231    // Special case: The user can all this function without supplying
232    // any authentication credentials.  If this happens, then just return
233    // Ok(None).  This is just to avoid having to write logic in application
234    // code to check if credentials have been set.
235    if self.name.is_none() && tkn.is_none() {
236      return Ok(None);
237    }
238
239    if let Some(tkn) = tkn {
240      // Authenticate using the token
241      token(conn, CredStore::Buf(tkn)).await?;
242
243      // Token authentications do not yield new tokens
244      return Ok(None);
245    }
246
247    // If a token authentication wasn't performed, then require an account.
248    if let Some(accname) = &self.name {
249      // Get required passphrase.  (Note that this will return error if a
250      // password can't be retrieved).
251      let pass = self.get_pass()?;
252
253      // Authenticate using account name and passphrase.  If a token file has
254      // been set, then at this point it is safe to assume the caller wants to
255      // request a token.
256      let opttkn = accpass(
257        conn,
258        accname,
259        CredStore::Buf(pass),
260        self.token_file.is_some()
261      )
262      .await?;
263
264      // If a token was returned, and a token file was specified, then attempt
265      // to write the token to the file.
266      if let Some(tkn) = opttkn {
267        // (token_file should always be Some() if an opttkn was returned)
268        if let Some(fname) = &self.token_file {
269          let mut f = File::create(fname)?;
270          f.write_all(tkn.as_bytes())?;
271        }
272        return Ok(Some(tkn));
273      }
274
275      // Successfully authenticated using user name and passphrase
276      return Ok(None);
277    }
278
279    // Token authetication failed and no account name/password was passed, so
280    // error out.
281    Err(Error::invalid_cred("Missing credentials"))
282  }
283}
284
285/// Choose where an a token/passphrase is fetched from.
286pub enum CredStore {
287  /// Credential is stored in a string.
288  Buf(String),
289
290  /// Credential is stored in a file.
291  File(PathBuf)
292}
293
294
295/// Attempt to authenticate using an authentication token.
296///
297/// The token can be stored in either a string buffer or file.
298pub async fn token<T, O>(
299  conn: &mut Framed<T, blather::Codec>,
300  tkn: O
301) -> Result<(), Error>
302where
303  O: Borrow<CredStore>,
304  T: AsyncRead + AsyncWrite + Unpin
305{
306  let tkn = match tkn.borrow() {
307    CredStore::Buf(s) => s.clone(),
308    CredStore::File(p) => {
309      if let Some(t) = utils::read_single_line(p) {
310        t
311      } else {
312        return Err(Error::invalid_cred("Unable to read token from file"));
313      }
314    }
315  };
316
317  let mut tg = Telegram::new_topic("Auth")?;
318  tg.add_param("Tkn", tkn)?;
319  crate::sendrecv(conn, &tg).await?;
320  Ok(())
321}
322
323
324/// Attempt to authenticate using an account name and a passphrase.
325///
326/// Optionally request an authentication token.
327///
328/// On success, return `Ok(None)` if authentication token was not requested.
329/// Return `Ok(Some(String))` with the token string if it was requested.
330pub async fn accpass<T, A, P>(
331  conn: &mut Framed<T, blather::Codec>,
332  accname: A,
333  pass: P,
334  reqtkn: bool
335) -> Result<Option<String>, Error>
336where
337  A: AsRef<str>,
338  P: Borrow<CredStore>,
339  T: AsyncRead + AsyncWrite + Unpin
340{
341  let mut tg = Telegram::new_topic("Auth")?;
342  tg.add_param("AccName", accname.as_ref())?;
343
344  let pass = match pass.borrow() {
345    CredStore::Buf(s) => s.clone(),
346    CredStore::File(p) => {
347      if let Some(pass) = utils::read_single_line(p) {
348        pass
349      } else {
350        return Err(Error::invalid_cred(
351          "Unable to read passphrase from file"
352        ));
353      }
354    }
355  };
356  tg.add_param("Pass", pass)?;
357
358  if reqtkn {
359    tg.add_param("ReqTkn", "True")?;
360  }
361  let params = crate::sendrecv(conn, &tg).await?;
362
363  if reqtkn {
364    let s = params.get_str("Tkn");
365    if let Some(s) = s {
366      Ok(Some(s.to_string()))
367    } else {
368      Ok(None)
369    }
370  } else {
371    Ok(None)
372  }
373}
374
375
376/// Return ownership of a connection to the built-in _unauthenticated_ account.
377///
378/// # Examples
379/// ```no_run
380/// use ddmw_client::{conn, auth};
381/// async fn test() {
382///   let pa = protwrap::ProtAddr::Tcp("127.0.0.1:8777".to_string());
383///   let auth = auth::Builder::new()
384///     .name("elena")
385///     .pass("secret")
386///     .build().expect("Unable to build Auth buffer");
387///   let mut frm = conn::connect(&pa, Some(&auth)).await
388///     .expect("Connection failed");
389///
390///   let w = conn::whoami(&mut frm).await.expect("whoami failed");
391///   assert_eq!(&w.name, "elena");
392///
393///   auth::unauthenticate(&mut frm).await.expect("unauthenticate failed");
394///   let w = conn::whoami(&mut frm).await.expect("whoami failed");
395///   assert_eq!(&w.name, "unauthenticated");
396/// }
397/// ```
398pub async fn unauthenticate<T>(
399  conn: &mut Framed<T, blather::Codec>
400) -> Result<(), Error>
401where
402  T: AsyncRead + AsyncWrite + Unpin
403{
404  let tg = Telegram::new_topic("Unauth")?;
405
406  crate::sendrecv(conn, &tg).await?;
407
408  Ok(())
409}
410
411// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :