From 8bf503df14a23d9c19fa70e18a134aab9c209fcb Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 29 Sep 2022 14:25:38 +0000 Subject: [PATCH 1/4] feat: #8 impl a credentials store --- Cargo.lock | 12 +++++++ Cargo.toml | 1 + src/handlers/response.rs | 2 ++ src/main.rs | 1 + src/stores/file.rs | 68 +++++++++++++++++++++++++++++++++++++ src/stores/mod.rs | 5 +++ src/stores/store.rs | 72 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+) create mode 100644 src/stores/file.rs create mode 100644 src/stores/mod.rs create mode 100644 src/stores/store.rs diff --git a/Cargo.lock b/Cargo.lock index 9eb98ec..f896643 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,17 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.0.0" @@ -504,6 +515,7 @@ name = "simple-auth" version = "0.1.0" dependencies = [ "async-std", + "async-trait", "json", "lazy_static", "regex", diff --git a/Cargo.toml b/Cargo.toml index 841e835..30c9f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ json = "0.12.4" lazy_static = "1.4.0" regex = "1" tokio = { version = "1.21.1", features = ["full"] } +async-trait = "0.1.57" [dependencies.async-std] version = "1.6" diff --git a/src/handlers/response.rs b/src/handlers/response.rs index c2a8d6f..23de852 100644 --- a/src/handlers/response.rs +++ b/src/handlers/response.rs @@ -68,6 +68,8 @@ impl From for HTTPResponse { return response; } + // TODO: impl a valid credentials in `Store` + let body = json::parse( r#"{"token": "header.payload.signature", "refresh": "header.payload.signature"}"#, ) diff --git a/src/main.rs b/src/main.rs index aa6e660..83c2b0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod stores; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, diff --git a/src/stores/file.rs b/src/stores/file.rs new file mode 100644 index 0000000..a9e7837 --- /dev/null +++ b/src/stores/file.rs @@ -0,0 +1,68 @@ +use async_trait::async_trait; +use json; +use json::object::Object; +use std::path::Path; + +use tokio::fs::File; +use tokio::io::AsyncReadExt; // for read_to_end() + +use super::store::{Credentials, Store}; + +/// FileStore references a `store` file +pub struct FileStore { + path: String, +} + +impl FileStore { + fn new(path: String) -> Self { + FileStore { path } + } + + async fn parse_contents(&self, mut file: File) -> Vec { + let mut contents = vec![]; + let byte_read = file.read_to_end(&mut contents).await; + if byte_read.is_err() { + eprintln!( + "error occurred while reading store file: {}, err={:?}", + self.path, + byte_read.err() + ); + } + contents + } +} + +#[async_trait] +impl Store for FileStore { + async fn is_auth(&self, data: &json::JsonValue) -> bool { + // ensure that the store file already exists even after its instanciation + if !Path::new(&self.path).is_file() { + eprintln!("{} path referencing file store does not exist", self.path); + return false; + } + + let credentials = Credentials::from(data); + if credentials.is_empty() { + eprintln!("unable to parse the credentials correctly from the incoming request"); + return false; + } + + let store = File::open(&self.path).await; + match store { + Ok(f) => { + let contents = self.parse_contents(f).await; + println!("file contents : {:?}", contents.len()); + return true; + } + Err(e) => eprintln!("error while opening the file {}, err={:?}", self.path, e), + } + false + } +} + +#[tokio::test] +async fn test_store() { + let store = FileStore::new("/tmp/thegux.pid".to_string()); + let data = json::parse(r#"{"access_token": "toto", "refresh_token": "tutu"}"#).unwrap(); + assert_eq!(store.is_auth(&data).await, false); +} diff --git a/src/stores/mod.rs b/src/stores/mod.rs new file mode 100644 index 0000000..5f1331c --- /dev/null +++ b/src/stores/mod.rs @@ -0,0 +1,5 @@ +mod file; +mod store; + +pub use file::FileStore; +pub use store::Store; diff --git a/src/stores/store.rs b/src/stores/store.rs new file mode 100644 index 0000000..a444225 --- /dev/null +++ b/src/stores/store.rs @@ -0,0 +1,72 @@ +use async_trait::async_trait; +use json; +use json::object::Object; + +#[async_trait] +pub trait Store { + async fn is_auth(&self, data: &json::JsonValue) -> bool; +} + +/// extract_json_value extracts String json value from a key +fn extract_json_value(data: &Object, key: &str) -> String { + if let Some(u) = data.get(key) { + match u.as_str() { + Some(s) => return s.to_string(), + None => return "".to_string(), + } + }; + "".to_string() +} + +#[derive(Default)] +pub struct Credentials { + pub username: String, + pub password: String, +} + +impl Credentials { + fn new(username: String, password: String) -> Self { + Credentials { username, password } + } + + pub fn is_empty(&self) -> bool { + self.username == "" || self.password == "" + } +} + +impl From<&json::JsonValue> for Credentials { + fn from(data: &json::JsonValue) -> Self { + let mut credentials = Credentials::default(); + match data { + json::JsonValue::Object(ref d) => { + credentials.username = extract_json_value(&d, "username"); + credentials.password = extract_json_value(&d, "password"); + } + _ => return credentials, + } + credentials + } +} + +#[test] +fn test_credentials() { + struct Expect { + data: json::JsonValue, + is_empty: bool, + } + let test_cases: [Expect; 2] = [ + Expect { + data: json::parse(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#).unwrap(), + is_empty: true + }, + Expect { + data: json::parse(r#"{"username":"toto","password": "tata"}"#).unwrap(), + is_empty: false + } + ]; + + for t in test_cases { + let credentials = Credentials::from(&t.data); + assert_eq!(t.is_empty, credentials.is_empty()) + } +} From 73059c724fc8335e8c6c4c6e1d48b98d97838374 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 29 Sep 2022 16:27:45 +0000 Subject: [PATCH 2/4] feat: #8 parse store file content and add test --- Cargo.toml | 3 ++ src/stores/file.rs | 85 +++++++++++++++++++++++++++++++------------- src/stores/store.rs | 6 ++-- tests/data/store.txt | 4 +++ 4 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 tests/data/store.txt diff --git a/Cargo.toml b/Cargo.toml index 30c9f67..8dfcac0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ regex = "1" tokio = { version = "1.21.1", features = ["full"] } async-trait = "0.1.57" +# useful for tests (embedded files should be delete in release ?) +#rust-embed="6.4.1" + [dependencies.async-std] version = "1.6" features = ["attributes"] diff --git a/src/stores/file.rs b/src/stores/file.rs index a9e7837..73dcf36 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -11,30 +11,65 @@ use super::store::{Credentials, Store}; /// FileStore references a `store` file pub struct FileStore { path: String, + credentials: Vec, } impl FileStore { fn new(path: String) -> Self { - FileStore { path } + FileStore { + path, + credentials: vec![], + } } - async fn parse_contents(&self, mut file: File) -> Vec { - let mut contents = vec![]; - let byte_read = file.read_to_end(&mut contents).await; - if byte_read.is_err() { - eprintln!( - "error occurred while reading store file: {}, err={:?}", - self.path, - byte_read.err() - ); + /// parse_contents loads and reads the file asynchonously + /// parses the file line by line to retrieve the credentials + async fn parse_contents(&mut self) { + let contents = tokio::fs::read_to_string(&self.path).await; + let mut credentials: Vec = vec![]; + match contents { + Ok(c) => { + let lines: Vec<&str> = c.split("\n").collect(); + for line in lines { + if line.starts_with("#") { + continue; + } + let line_split: Vec<&str> = line.split(":").collect(); + if line_split.len() != 2 { + continue; + } + credentials.push(Credentials::new( + line_split[0].to_string(), + line_split[1].to_string(), + )); + } + } + Err(e) => { + eprintln!( + "error occurred while reading store file: {}, err={:?}", + self.path, e + ); + } } - contents + self.credentials = credentials; + } + + fn auth(&self, username: String, password: String) -> bool { + let credentials: Vec<&Credentials> = self + .credentials + .iter() + .filter(|x| x.username == username && x.password == password) + .collect(); + if credentials.len() == 1 { + return true; + } + false } } #[async_trait] impl Store for FileStore { - async fn is_auth(&self, data: &json::JsonValue) -> bool { + async fn is_auth(&mut self, data: &json::JsonValue) -> bool { // ensure that the store file already exists even after its instanciation if !Path::new(&self.path).is_file() { eprintln!("{} path referencing file store does not exist", self.path); @@ -47,22 +82,22 @@ impl Store for FileStore { return false; } - let store = File::open(&self.path).await; - match store { - Ok(f) => { - let contents = self.parse_contents(f).await; - println!("file contents : {:?}", contents.len()); - return true; - } - Err(e) => eprintln!("error while opening the file {}, err={:?}", self.path, e), - } - false + let contents = self.parse_contents().await; + + self.auth(credentials.username, credentials.password) } } #[tokio::test] async fn test_store() { - let store = FileStore::new("/tmp/thegux.pid".to_string()); - let data = json::parse(r#"{"access_token": "toto", "refresh_token": "tutu"}"#).unwrap(); - assert_eq!(store.is_auth(&data).await, false); + use std::env; + + let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); + // TODO: path::Path should be better + let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt"); + + let mut store = FileStore::new(store_path); + + let data = json::parse(r#"{"username": "toto", "password": "tata"}"#).unwrap(); + assert_eq!(store.is_auth(&data).await, true); } diff --git a/src/stores/store.rs b/src/stores/store.rs index a444225..50e679b 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -4,7 +4,7 @@ use json::object::Object; #[async_trait] pub trait Store { - async fn is_auth(&self, data: &json::JsonValue) -> bool; + async fn is_auth(&mut self, data: &json::JsonValue) -> bool; } /// extract_json_value extracts String json value from a key @@ -18,14 +18,14 @@ fn extract_json_value(data: &Object, key: &str) -> String { "".to_string() } -#[derive(Default)] +#[derive(Default, Debug)] pub struct Credentials { pub username: String, pub password: String, } impl Credentials { - fn new(username: String, password: String) -> Self { + pub fn new(username: String, password: String) -> Self { Credentials { username, password } } diff --git a/tests/data/store.txt b/tests/data/store.txt new file mode 100644 index 0000000..a9b0316 --- /dev/null +++ b/tests/data/store.txt @@ -0,0 +1,4 @@ +# this a test password storage with password in clear +# need to be updated in the future to encrypt or hash the password +# : +toto:tata From 384e8685012e150c0cd21387cdd386a58d9bc0c4 Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 3 Oct 2022 15:09:44 +0000 Subject: [PATCH 3/4] #8 return a 403 if credentials are invalid + adjust corresponding test --- src/handlers/request.rs | 4 ++ src/handlers/response.rs | 87 +++++++++++++++++++++++++---------- src/main.rs | 2 +- src/stores/file.rs | 2 +- tests/bash/curling.bash | 6 +-- tests/python/test_requests.py | 22 +++++++++ 6 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/handlers/request.rs b/src/handlers/request.rs index 4911bd4..ef1b4fe 100644 --- a/src/handlers/request.rs +++ b/src/handlers/request.rs @@ -156,6 +156,10 @@ impl HTTPBody { fn new(data: json::JsonValue) -> HTTPBody { HTTPBody { data } } + + pub fn get_data(&self) -> &json::JsonValue { + &self.data + } } impl TryFrom for HTTPBody { diff --git a/src/handlers/response.rs b/src/handlers/response.rs index 23de852..a7d14ae 100644 --- a/src/handlers/response.rs +++ b/src/handlers/response.rs @@ -3,13 +3,17 @@ //! message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages //! NOTE: only few parts of the specification has been implemented -use json; - use crate::handlers::request::{HTTPRequest, HTTPVersion}; +use async_trait::async_trait; +use json; +// add the Store trait to be used by `FileStore` +use crate::stores::FileStore; +use crate::stores::Store; enum HTTPStatusCode { Http200, Http400, + Http403, Http404, Http500, } @@ -20,6 +24,7 @@ impl Into for HTTPStatusCode { Self::Http200 => "200".to_string(), Self::Http400 => "400".to_string(), Self::Http404 => "404".to_string(), + Self::Http403 => "403".to_string(), Self::Http500 => "500".to_string(), } } @@ -47,6 +52,12 @@ impl Into for HTTPStatusLine { } } +impl HTTPStatusLine { + fn set_status_code(&mut self, code: HTTPStatusCode) { + self.status_code = code; + } +} + pub struct HTTPResponse { status_line: HTTPStatusLine, body: json::JsonValue, @@ -61,28 +72,6 @@ impl Default for HTTPResponse { } } -impl From for HTTPResponse { - fn from(request: HTTPRequest) -> Self { - let mut response = HTTPResponse::default(); - if !request.is_valid() { - return response; - } - - // TODO: impl a valid credentials in `Store` - - let body = json::parse( - r#"{"token": "header.payload.signature", "refresh": "header.payload.signature"}"#, - ) - .unwrap(); - - response.status_line.version = request.start_line.version; - response.status_line.status_code = HTTPStatusCode::Http200; - response.body = body; - - response - } -} - impl Into for HTTPResponse { fn into(self) -> String { // move `self.body` into a new var @@ -98,3 +87,53 @@ impl Into for HTTPResponse { ) } } + +impl HTTPResponse { + // `From` could be used instead of forcing it like this + // it fails using `async_trait` attributes (only custom traits work ?) + pub async fn from(request: HTTPRequest) -> Self { + let mut response = HTTPResponse::default(); + if !request.is_valid() { + return response; + } + + // empty body -> invalid request (credentials needed) + if let None = request.body { + return Self::as_403(); + } + + // TODO: path to `store.txt` must not be hardcoded, should be in a config file and load at + // runtime + let mut store = FileStore::new("tests/data/store.txt".to_string()); + let body = request.body.unwrap(); + let is_auth = store.is_auth(&body.get_data()).await; + + if !is_auth { + return Self::as_403(); + } + + // TODO: must be a valid JWT (to implement) + let body = json::parse( + r#"{"token": "header.payload.signature", "refresh": "header.payload.signature"}"#, + ) + .unwrap(); + + response.status_line.version = request.start_line.version; + response.status_line.status_code = HTTPStatusCode::Http200; + response.body = body; + + response + } + + /// as_403 generates a 403 response with a correct error message + pub fn as_403() -> Self { + let mut response = HTTPResponse { + status_line: HTTPStatusLine::default(), + body: json::parse(r#"{"error": "invalid credentials"}"#).unwrap(), + }; + response + .status_line + .set_status_code(HTTPStatusCode::Http403); + response + } +} diff --git a/src/main.rs b/src/main.rs index 83c2b0d..f753f09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ async fn handle_connection(mut stream: TcpStream) { let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); let request = handle_request(request_string); - let response = HTTPResponse::from(request); + let response = HTTPResponse::from(request).await; let response_str: String = response.into(); stream.write(response_str.as_bytes()).await.unwrap(); diff --git a/src/stores/file.rs b/src/stores/file.rs index 73dcf36..0a56da4 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -15,7 +15,7 @@ pub struct FileStore { } impl FileStore { - fn new(path: String) -> Self { + pub fn new(path: String) -> Self { FileStore { path, credentials: vec![], diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 3021842..d144993 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -11,15 +11,15 @@ URL="https://dev.thegux.fr" for i in {0..10} do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}') - if [ $http_response != "200" ] + if [ $http_response != "403" ] then echo "bad http status code : ${http_response}, expect 200" exit 1 fi - if [ "$(cat response.txt | jq -r '.token')" != "header.payload.signature" ] + if [ "$(cat response.txt | jq -r '.error')" != "invalid credentials" ] then - echo "bad data returned, expect : ok" + echo "bad data returned, expect : invalid credentials" exit 1 fi done diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 95ec7a7..d17f41e 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -36,6 +36,28 @@ class TestResponse(TestCase): resp.json()["token"], "header.payload.signature", "bad status returned" ) + def test_no_credentials(self): + resp = requests.post(URL + "/get/") + self.assertEqual(resp.status_code, 403, "bad status code returned") + self.assertIsNotNone(resp.json(), "response data must not be empty") + self.assertEqual( + resp.json()["error"], + "invalid credentials", + "invalid error message returned", + ) + + def test_bad_credentials(self): + resp = requests.post( + URL + "/get/", json={"username": "tutu", "password": "titi"} + ) + self.assertEqual(resp.status_code, 403, "bas status code returned") + self.assertIsNotNone(resp.json(), "response data must not be empty") + self.assertEqual( + resp.json()["error"], + "invalid credentials", + "invalid error message returned", + ) + def test_bad_target(self): resp = requests.post( URL + "/token/", json={"username": "toto", "password": "tata"} From 13b515c9c7ea7f1f4f8ffdc91fa79facf619e6ce Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 3 Oct 2022 15:34:37 +0000 Subject: [PATCH 4/4] #8 update the doc --- src/handlers/mod.rs | 2 ++ src/handlers/request.rs | 12 ++++++------ src/handlers/response.rs | 9 ++++++--- src/main.rs | 1 + src/stores/file.rs | 5 +++-- src/stores/mod.rs | 6 ++++++ src/stores/store.rs | 2 +- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index b5c833b..c6e09ea 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,3 +1,5 @@ +//! handlers module includes tools to parse an HTTP request and build and HTTP response + pub mod request; pub mod response; diff --git a/src/handlers/request.rs b/src/handlers/request.rs index ef1b4fe..2cec0a5 100644 --- a/src/handlers/request.rs +++ b/src/handlers/request.rs @@ -99,7 +99,7 @@ impl HTTPStartLine { )) } - /// check_method checks if the start_line method is in a predefined HTTP method list + /// checks if the start_line method is in a predefined HTTP method list fn check_method(method: &String) -> bool { for m in HTTP_METHODS.iter() { if m.to_string() == *method { @@ -109,7 +109,7 @@ impl HTTPStartLine { false } - /// check_target checks if the start_line target is in a predefined HTTP target whitelist + /// checks if the start_line target is in a predefined HTTP target whitelist fn check_target(target: &String) -> bool { for t in HTTP_TARGETS.iter() { if t.to_string() == *target { @@ -145,7 +145,7 @@ impl Into for HTTPStartLine { } } -/// HTTPBody represents http request body +/// represents an HTTP request body /// for simplicity, only json body is accepted #[derive(Debug)] pub struct HTTPBody { @@ -176,7 +176,7 @@ impl TryFrom for HTTPBody { } } -/// Request defined the HTTP request +/// Represents an HTTP request (headers are not parsed) #[derive(Debug)] pub struct HTTPRequest { pub start_line: HTTPStartLine, @@ -209,7 +209,7 @@ impl HTTPRequest { Ok((start_line, request_parts, body)) } - /// parse parses the request by spliting the incoming request with the separator `\r\n` + /// parse the request by spliting the incoming request with the separator `\r\n` fn parse(request: &str) -> Result { let request = request.to_string(); @@ -264,7 +264,7 @@ impl From<&str> for HTTPRequest { } pub fn handle_request(request: &str) -> HTTPRequest { - return HTTPRequest::from(request); + HTTPRequest::from(request) } #[test] diff --git a/src/handlers/response.rs b/src/handlers/response.rs index a7d14ae..6142725 100644 --- a/src/handlers/response.rs +++ b/src/handlers/response.rs @@ -58,6 +58,8 @@ impl HTTPStatusLine { } } +/// represents an HTTP response (headers are not parsed) +/// NOTE: for simplicity, only JSON body are accepted pub struct HTTPResponse { status_line: HTTPStatusLine, body: json::JsonValue, @@ -89,8 +91,9 @@ impl Into for HTTPResponse { } impl HTTPResponse { - // `From` could be used instead of forcing it like this - // it fails using `async_trait` attributes (only custom traits work ?) + /// creates a response from the incoming `Request` + /// `From` could be used instead of forcing it like this + /// it fails using `async_trait` attributes (only custom traits work ?) pub async fn from(request: HTTPRequest) -> Self { let mut response = HTTPResponse::default(); if !request.is_valid() { @@ -125,7 +128,7 @@ impl HTTPResponse { response } - /// as_403 generates a 403 response with a correct error message + /// generates a 403 response with a correct error message pub fn as_403() -> Self { let mut response = HTTPResponse { status_line: HTTPStatusLine::default(), diff --git a/src/main.rs b/src/main.rs index f753f09..93b29cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ async fn main() { } } +/// parses the incoming request (partial spec implementation) and build an HTTP response async fn handle_connection(mut stream: TcpStream) { let mut buffer: [u8; 1024] = [0; 1024]; let n = stream.read(&mut buffer).await.unwrap(); diff --git a/src/stores/file.rs b/src/stores/file.rs index 0a56da4..4aef2de 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -8,7 +8,7 @@ use tokio::io::AsyncReadExt; // for read_to_end() use super::store::{Credentials, Store}; -/// FileStore references a `store` file +/// references a credentials store file pub struct FileStore { path: String, credentials: Vec, @@ -22,7 +22,7 @@ impl FileStore { } } - /// parse_contents loads and reads the file asynchonously + /// loads and reads the file asynchonously /// parses the file line by line to retrieve the credentials async fn parse_contents(&mut self) { let contents = tokio::fs::read_to_string(&self.path).await; @@ -54,6 +54,7 @@ impl FileStore { self.credentials = credentials; } + /// checks if the credentials exist in the `FileStore` fn auth(&self, username: String, password: String) -> bool { let credentials: Vec<&Credentials> = self .credentials diff --git a/src/stores/mod.rs b/src/stores/mod.rs index 5f1331c..470c347 100644 --- a/src/stores/mod.rs +++ b/src/stores/mod.rs @@ -1,3 +1,9 @@ +//! store module lists interfaces available to check request credentials +//! each store must implement the trait `is_auth` +//! two stores are available : +//! * `FileStore`: credentials stored in a text file (like **/etc/passwd**) +//! * `DBStore`: credentials stored in a database (TODO) + mod file; mod store; diff --git a/src/stores/store.rs b/src/stores/store.rs index 50e679b..f1c0a42 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -7,7 +7,7 @@ pub trait Store { async fn is_auth(&mut self, data: &json::JsonValue) -> bool; } -/// extract_json_value extracts String json value from a key +/// extracts `String` json value from a key fn extract_json_value(data: &Object, key: &str) -> String { if let Some(u) = data.get(key) { match u.as_str() {