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 :