diff --git a/Cargo.lock b/Cargo.lock
index dc7a57f..9b5be8a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -256,9 +256,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
+checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
@@ -413,6 +413,15 @@ dependencies = [
"wyz",
]
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -422,6 +431,17 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "blowfish"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b"
+dependencies = [
+ "byteorder",
+ "cipher",
+ "opaque-debug",
+]
+
[[package]]
name = "bmcd"
version = "1.3.0"
@@ -434,8 +454,10 @@ dependencies = [
"futures",
"if-addrs",
"log",
+ "mockall",
"nix 0.26.4",
"openssl",
+ "pwhash",
"rand 0.8.5",
"serde",
"serde_json",
@@ -486,6 +508,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
[[package]]
name = "bytes"
version = "1.5.0"
@@ -529,20 +557,29 @@ dependencies = [
"windows-targets 0.48.5",
]
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "clap"
-version = "4.4.4"
+version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
+checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.4.4"
+version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
+checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
dependencies = [
"anstream",
"anstyle",
@@ -639,6 +676,16 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "crypto-mac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
[[package]]
name = "deranged"
version = "0.3.8"
@@ -658,16 +705,43 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "block-buffer",
+ "block-buffer 0.10.4",
"crypto-common",
]
+[[package]]
+name = "downcast"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
[[package]]
name = "encoding_rs"
version = "0.8.33"
@@ -738,6 +812,15 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -768,6 +851,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fragile"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
+
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -951,6 +1040,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+]
+
[[package]]
name = "http"
version = "0.2.9"
@@ -1069,6 +1168,15 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.9"
@@ -1162,6 +1270,17 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+[[package]]
+name = "md-5"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
+dependencies = [
+ "block-buffer 0.9.0",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
[[package]]
name = "memchr"
version = "2.6.3"
@@ -1223,6 +1342,33 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "mockall"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96"
+dependencies = [
+ "cfg-if",
+ "downcast",
+ "fragile",
+ "lazy_static",
+ "mockall_derive",
+ "predicates",
+ "predicates-tree",
+]
+
+[[package]]
+name = "mockall_derive"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "nix"
version = "0.23.2"
@@ -1249,6 +1395,12 @@ dependencies = [
"pin-utils",
]
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
[[package]]
name = "num-traits"
version = "0.2.16"
@@ -1313,6 +1465,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
[[package]]
name = "openssl"
version = "0.10.57"
@@ -1410,6 +1568,36 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+[[package]]
+name = "predicates"
+version = "2.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
+dependencies = [
+ "difflib",
+ "float-cmp",
+ "itertools",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -1429,6 +1617,21 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pwhash"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56"
+dependencies = [
+ "blowfish",
+ "byteorder",
+ "hmac",
+ "md-5",
+ "rand 0.8.5",
+ "sha-1",
+ "sha2",
+]
+
[[package]]
name = "quote"
version = "1.0.33"
@@ -1575,9 +1778,9 @@ dependencies = [
[[package]]
name = "rockfile"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e492654d9f624d21df4f0eb765c9174a4a577c4d47680803ffe09b6ae55b3d02"
+checksum = "7e89194db9752ec1f052472debd3990f77bec4ca0732d9dd28163f95fed1455a"
dependencies = [
"bytes",
]
@@ -1622,9 +1825,9 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.13"
+version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
+checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno",
@@ -1647,9 +1850,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.5"
+version = "0.101.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
+checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
dependencies = [
"ring",
"untrusted",
@@ -1688,9 +1891,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.18"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
[[package]]
name = "serde"
@@ -1748,15 +1951,41 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "sha-1"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
[[package]]
name = "sha1"
-version = "0.10.5"
+version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
]
[[package]]
@@ -1791,9 +2020,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "socket2"
@@ -1817,6 +2046,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -1855,6 +2090,12 @@ dependencies = [
"remove_dir_all",
]
+[[package]]
+name = "termtree"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
+
[[package]]
name = "thiserror"
version = "1.0.48"
@@ -1877,9 +2118,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
+checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
dependencies = [
"deranged",
"itoa",
@@ -1892,15 +2133,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.1"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
-version = "0.2.14"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
+checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
@@ -1964,9 +2205,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.8"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
diff --git a/bmcd/Cargo.toml b/bmcd/Cargo.toml
index e368561..cd1279d 100644
--- a/bmcd/Cargo.toml
+++ b/bmcd/Cargo.toml
@@ -15,6 +15,8 @@ serde_yaml = "0.9.25"
tpi_rs = { path = "../tpi_rs" }
clap = { version = "4.4.2", features = ["cargo"] }
openssl = "0.10.57"
+rand = "0.8.5"
+pwhash = "1.0.0"
anyhow.workspace = true
log.workspace = true
@@ -23,4 +25,6 @@ tokio.workspace = true
tokio-util.workspace = true
futures.workspace = true
serde.workspace = true
-rand = "0.8.5"
+
+[dev-dependencies]
+mockall = "0.11.4"
diff --git a/bmcd/src/authentication/authentication_context.rs b/bmcd/src/authentication/authentication_context.rs
new file mode 100644
index 0000000..28aec72
--- /dev/null
+++ b/bmcd/src/authentication/authentication_context.rs
@@ -0,0 +1,317 @@
+use super::authentication_errors::AuthenticationError;
+use super::authentication_errors::TokenError;
+use super::passwd_validator::PasswordValidator;
+use super::passwd_validator::UnixValidator;
+use actix_web::dev::ServiceRequest;
+use actix_web::http::header;
+use actix_web::http::header::HeaderValue;
+use futures::StreamExt;
+use rand::distributions::Alphanumeric;
+use rand::thread_rng;
+use rand::Rng;
+use serde::Deserialize;
+use serde::Serialize;
+use std::collections::HashMap;
+use std::marker::PhantomData;
+use tokio::{
+ sync::RwLock,
+ time::{Duration, Instant},
+};
+
+pub struct AuthenticationContext
+where
+ P: PasswordValidator + 'static,
+{
+ token_store: RwLock>,
+ passwds: HashMap,
+ password_validator: PhantomData,
+ expiration_timeout: Duration,
+}
+
+impl
AuthenticationContext
+where
+ P: PasswordValidator + 'static,
+{
+ pub fn with_unix_validator(
+ password_entries: impl Iterator- ,
+ expiration_timeout: Duration,
+ ) -> AuthenticationContext {
+ AuthenticationContext:: {
+ token_store: RwLock::new(HashMap::new()),
+ passwds: HashMap::from_iter(password_entries),
+ password_validator: PhantomData::,
+ expiration_timeout,
+ }
+ }
+
+ fn parse_http_token(auth_string: &str) -> Result<&str, TokenError> {
+ auth_string
+ .split_once(' ')
+ .ok_or(TokenError::HttpParseError(auth_string.to_string()))
+ .and_then(|(_, token)| {
+ (!token.is_empty())
+ .then_some(token)
+ .ok_or(TokenError::Empty)
+ })
+ }
+
+ async fn verify_token(&self, auth_string: &str) -> Result<(), TokenError> {
+ let token = Self::parse_http_token(auth_string)?;
+
+ let store = self.token_store.read().await;
+ let Some(expiration) = store.get(token).copied() else {
+ return Err(TokenError::NoMatch(token.to_string()));
+ };
+
+ let duration = Instant::now().saturating_duration_since(expiration);
+ if duration > self.expiration_timeout {
+ // upgrade to write lock
+ drop(store);
+ self.token_store.write().await.remove(token);
+ return Err(TokenError::Expired(expiration));
+ }
+
+ Ok(())
+ }
+
+ pub async fn authorize_request(&self, request: &ServiceRequest) -> Result<(), TokenError> {
+ let auth = request
+ .headers()
+ .get(header::AUTHORIZATION)
+ .map(HeaderValue::to_str)
+ .ok_or(TokenError::Empty)?;
+
+ self.verify_token(auth.unwrap_or_default()).await
+ }
+
+ pub async fn authenticate_request(
+ &self,
+ request: &mut ServiceRequest,
+ ) -> Result<(String, Instant), AuthenticationError> {
+ let mut buffer = Vec::new();
+ while let Some(Ok(bytes)) = request.parts_mut().1.next().await {
+ buffer.extend_from_slice(&bytes);
+ }
+
+ let credentials = serde_json::from_slice::(&buffer)?;
+ let Some(pass) = self.passwds.get(&credentials.username) else {
+ log::debug!("user {} not in database",&credentials.username);
+ return Err(AuthenticationError::IncorrectCredentials);
+ };
+
+ P::validate(pass, &credentials.password)?;
+
+ let token: String = thread_rng()
+ .sample_iter(&Alphanumeric)
+ .take(64)
+ .map(char::from)
+ .collect();
+ let expires = Instant::now() + self.expiration_timeout;
+ self.token_store
+ .write()
+ .await
+ .insert(token.clone(), expires);
+ Ok((token, expires))
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct Login {
+ username: String,
+ password: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ops::Sub;
+
+ use super::*;
+ use actix_web::{
+ http::header::AUTHORIZATION,
+ test::{self},
+ };
+
+ struct DummyValidator {}
+ impl PasswordValidator for DummyValidator {
+ fn validate(
+ _: &str,
+ _: &str,
+ ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> {
+ Ok(())
+ }
+ }
+
+ struct FalseValidator {}
+ impl PasswordValidator for FalseValidator {
+ fn validate(
+ _: &str,
+ _: &str,
+ ) -> Result<(), crate::authentication::authentication_errors::AuthenticationError> {
+ Err(AuthenticationError::IncorrectCredentials)
+ }
+ }
+
+ fn build_test_context(
+ token_data: impl IntoIterator
- ,
+ user_data: impl IntoIterator
- ,
+ ) -> AuthenticationContext {
+ AuthenticationContext {
+ token_store: RwLock::new(HashMap::from_iter(token_data)),
+ passwds: HashMap::from_iter(user_data),
+ password_validator: PhantomData::,
+ expiration_timeout: Duration::from_secs(20),
+ }
+ }
+
+ fn build_false_context(
+ token_data: impl IntoIterator
- ,
+ user_data: impl IntoIterator
- ,
+ ) -> AuthenticationContext {
+ AuthenticationContext {
+ token_store: RwLock::new(HashMap::from_iter(token_data)),
+ passwds: HashMap::from_iter(user_data),
+ password_validator: PhantomData::,
+ expiration_timeout: Duration::from_secs(20),
+ }
+ }
+
+ #[actix_web::test]
+ async fn test_wrongly_encoded_token() {
+ let context = build_test_context(Vec::new(), Vec::new());
+ assert!(matches!(
+ context.verify_token("").await.unwrap_err(),
+ TokenError::HttpParseError(_),
+ ));
+ assert!(matches!(
+ context.verify_token("Bearernospace").await.unwrap_err(),
+ TokenError::HttpParseError(_),
+ ));
+ assert_eq!(
+ TokenError::Empty,
+ context.verify_token("Bearer ").await.unwrap_err()
+ );
+
+ let req = test::TestRequest::default().to_srv_request();
+ assert_eq!(
+ TokenError::Empty,
+ context.authorize_request(&req).await.unwrap_err()
+ );
+ }
+
+ #[actix_web::test]
+ async fn test_token_failures() {
+ let context = build_test_context(
+ [
+ ("123".to_string(), Instant::now()),
+ ("2".to_string(), Instant::now().sub(Duration::from_secs(20))),
+ ],
+ Vec::new(),
+ );
+
+ let req = test::TestRequest::default()
+ .insert_header((AUTHORIZATION, "Bearer 1234"))
+ .to_srv_request();
+
+ assert_eq!(
+ context.authorize_request(&req).await.unwrap_err(),
+ TokenError::NoMatch("1234".to_string())
+ );
+
+ let req = test::TestRequest::default()
+ .insert_header((AUTHORIZATION, "Bearer 2"))
+ .to_srv_request();
+ assert!(matches!(
+ context.authorize_request(&req).await.unwrap_err(),
+ TokenError::Expired(x) if Instant::now().saturating_duration_since(x) > Duration::from_secs(20)
+ ));
+
+ // After expired error, the token gets removed. Subsequent calls for that token will
+ // therefore return "NoMatch"
+ let req = test::TestRequest::default()
+ .insert_header((AUTHORIZATION, "Bearer 2"))
+ .to_srv_request();
+ assert_eq!(
+ context.authorize_request(&req).await.unwrap_err(),
+ TokenError::NoMatch("2".to_string())
+ );
+ }
+
+ #[actix_web::test]
+ async fn test_happy_flow() {
+ let context = build_test_context(
+ [
+ ("123".to_string(), Instant::now()),
+ ("2".to_string(), Instant::now().sub(Duration::from_secs(20))),
+ ],
+ Vec::new(),
+ );
+ let req = test::TestRequest::default()
+ .insert_header((AUTHORIZATION, "Bearer 123"))
+ .to_srv_request();
+ assert_eq!(Ok(()), context.authorize_request(&req).await);
+ }
+
+ #[actix_web::test]
+ async fn authentication_errors() {
+ let context = build_test_context(
+ Vec::new(),
+ [("test_user".to_string(), "password".to_string())],
+ );
+
+ let mut req = test::TestRequest::default()
+ .set_payload("{not a valid json")
+ .to_srv_request();
+ assert!(matches!(
+ context.authenticate_request(&mut req).await.unwrap_err(),
+ AuthenticationError::ParseError(_)
+ ));
+
+ let mut req = test::TestRequest::default()
+ .set_json(Login {
+ username: "John".to_string(),
+ password: "1234".to_string(),
+ })
+ .to_srv_request();
+ assert!(matches!(
+ context.authenticate_request(&mut req).await.unwrap_err(),
+ AuthenticationError::IncorrectCredentials
+ ));
+ }
+
+ #[actix_web::test]
+ async fn invalid_password() {
+ let context = build_false_context(
+ Vec::new(),
+ [("test_user".to_string(), "password".to_string())],
+ );
+ let mut req = test::TestRequest::default()
+ .set_json(Login {
+ username: "test_user".to_string(),
+ password: "1234".to_string(),
+ })
+ .to_srv_request();
+ assert!(matches!(
+ context.authenticate_request(&mut req).await.unwrap_err(),
+ AuthenticationError::IncorrectCredentials
+ ));
+ }
+
+ #[actix_web::test]
+ async fn pass_authentication() {
+ let context = build_test_context(
+ Vec::new(),
+ [("test_user".to_string(), "password".to_string())],
+ );
+ let mut req = test::TestRequest::default()
+ .set_json(Login {
+ username: "test_user".to_string(),
+ password: "password".to_string(),
+ })
+ .to_srv_request();
+
+ assert!(matches!(
+ context.authenticate_request(&mut req).await.unwrap(),
+ (token, _) if !token.is_empty()
+ ));
+ }
+}
diff --git a/bmcd/src/authentication/authentication_errors.rs b/bmcd/src/authentication/authentication_errors.rs
new file mode 100644
index 0000000..1830558
--- /dev/null
+++ b/bmcd/src/authentication/authentication_errors.rs
@@ -0,0 +1,60 @@
+use std::fmt::Display;
+use tokio::time::Instant;
+
+#[derive(Debug)]
+pub enum AuthenticationError {
+ ParseError(serde_json::Error),
+ IncorrectCredentials,
+ InvalidToken(TokenError),
+}
+
+impl Display for AuthenticationError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ AuthenticationError::ParseError(e) => {
+ write!(f, "error trying to parse credentials: {}", e)
+ }
+ AuthenticationError::IncorrectCredentials => write!(f, "credentials incorrect"),
+ AuthenticationError::InvalidToken(token_error) => token_error.fmt(f),
+ }
+ }
+}
+
+impl std::error::Error for AuthenticationError {}
+
+impl From for AuthenticationError {
+ fn from(value: serde_json::Error) -> Self {
+ Self::ParseError(value)
+ }
+}
+
+impl From for AuthenticationError {
+ fn from(value: TokenError) -> Self {
+ Self::InvalidToken(value)
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum TokenError {
+ Expired(Instant),
+ NoMatch(String),
+ HttpParseError(String),
+ Empty,
+}
+
+impl Display for TokenError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ TokenError::Expired(instant) => write!(
+ f,
+ "token expired {}s ago",
+ Instant::now().duration_since(*instant).as_secs()
+ ),
+ TokenError::NoMatch(token) => write!(f, "token {} is not registerd", token),
+ TokenError::Empty => write!(f, "no authorization token provided"),
+ TokenError::HttpParseError(token) => write!(f, "cannot parse token {}", token),
+ }
+ }
+}
+
+impl std::error::Error for TokenError {}
diff --git a/bmcd/src/authentication/authentication_service.rs b/bmcd/src/authentication/authentication_service.rs
new file mode 100644
index 0000000..5995f04
--- /dev/null
+++ b/bmcd/src/authentication/authentication_service.rs
@@ -0,0 +1,124 @@
+use super::{authentication_context::AuthenticationContext, passwd_validator::UnixValidator};
+use actix_web::{
+ body::{EitherBody, MessageBody},
+ dev::{Service, ServiceRequest, ServiceResponse},
+ http::header,
+ Error, HttpRequest, HttpResponse,
+};
+use futures::future::LocalBoxFuture;
+use serde_json::json;
+use std::{rc::Rc, sync::Arc, time::Duration};
+use tokio::time::Instant;
+
+#[derive(Clone)]
+pub struct AuthenticationService
{
+ service: Rc,
+ context: Arc>,
+ authentication_path: &'static str,
+}
+
+impl AuthenticationService {
+ pub fn new(
+ service: Rc,
+ context: Arc>,
+ authentication_path: &'static str,
+ ) -> Self {
+ AuthenticationService {
+ service,
+ context,
+ authentication_path,
+ }
+ }
+}
+
+impl Service for AuthenticationService
+where
+ S: Service, Error = Error> + 'static,
+ S::Future: 'static,
+ B: MessageBody + 'static,
+{
+ type Response = ServiceResponse>;
+ type Error = Error;
+ type Future = LocalBoxFuture<'static, Result>;
+
+ actix_web::dev::forward_ready!(service);
+
+ fn call(&self, mut request: ServiceRequest) -> Self::Future {
+ let service = self.service.clone();
+ let context = self.context.clone();
+ let auth_path = self.authentication_path;
+ Box::pin(async move {
+ // authentication request
+ let request_clone = request.request().clone();
+ if request.request().uri().path() == auth_path {
+ log::debug!("authentication request");
+ match context.authenticate_request(&mut request).await {
+ Ok((token, expires)) => {
+ return authenticated_response(
+ request_clone,
+ token,
+ expires.saturating_duration_since(Instant::now()),
+ );
+ }
+ Err(error) => return forbidden_response(request_clone, error),
+ }
+ }
+
+ // authorize request
+ if let Err(e) = context.authorize_request(&request).await {
+ unauthorized_response(request_clone, e)
+ } else {
+ service
+ .call(request)
+ .await
+ .map(ServiceResponse::map_into_left_body)
+ }
+ })
+ }
+}
+
+fn forbidden_response(
+ request: HttpRequest,
+ response_text: E,
+) -> Result>, Error> {
+ Ok(ServiceResponse::new(
+ request,
+ HttpResponse::Forbidden()
+ .body(response_text.to_string())
+ .map_into_right_body(),
+ ))
+}
+
+fn authenticated_response(
+ request: HttpRequest,
+ token: String,
+ duration: Duration,
+) -> Result>, Error> {
+ let body = json! {{
+ "access_token" : token,
+ "token_type" : "Bearer",
+ "expires_in": duration.as_secs(),
+ }};
+
+ Ok(ServiceResponse::new(
+ request,
+ HttpResponse::Ok()
+ .insert_header(("X-Auth-Token", token))
+ .body(body.to_string())
+ .map_into_right_body(),
+ ))
+}
+
+fn unauthorized_response(
+ request: HttpRequest,
+ response_text: E,
+) -> Result>, Error> {
+ let bearer_str = format!(
+ "Bearer error=invalid_token, error_description={}",
+ response_text.to_string()
+ );
+ let response = HttpResponse::Unauthorized()
+ .insert_header((header::WWW_AUTHENTICATE, bearer_str))
+ .body(response_text.to_string());
+ Ok(ServiceResponse::new(request, response)).map(ServiceResponse::map_into_right_body)
+}
diff --git a/bmcd/src/authentication/linux_authenticator.rs b/bmcd/src/authentication/linux_authenticator.rs
new file mode 100644
index 0000000..0e6b863
--- /dev/null
+++ b/bmcd/src/authentication/linux_authenticator.rs
@@ -0,0 +1,90 @@
+use super::{
+ authentication_context::AuthenticationContext, authentication_service::AuthenticationService,
+ passwd_validator::UnixValidator,
+};
+use actix_web::{
+ body::{EitherBody, MessageBody},
+ dev::{Service, ServiceRequest, ServiceResponse, Transform},
+ Error,
+};
+use std::{
+ future::{ready, Ready},
+ io,
+ time::Duration,
+};
+use std::{rc::Rc, sync::Arc};
+use tokio::{
+ fs::OpenOptions,
+ io::{AsyncBufReadExt, BufReader},
+};
+
+type LinuxContext = AuthenticationContext;
+
+pub struct LinuxAuthenticator {
+ context: Arc,
+ authentication_path: &'static str,
+}
+
+impl LinuxAuthenticator {
+ pub async fn new(authentication_path: &'static str) -> io::Result {
+ let password_entries = Self::parse_shadow_file().await?;
+ Ok(Self {
+ context: Arc::new(LinuxContext::with_unix_validator(
+ password_entries,
+ Duration::from_secs(24 * 60 * 60),
+ )),
+ authentication_path,
+ })
+ }
+}
+
+impl LinuxAuthenticator {
+ async fn parse_shadow_file() -> io::Result> {
+ let file = OpenOptions::new().read(true).open("/etc/shadow").await?;
+
+ let mut password_hashes: Vec<(String, String)> = Vec::new();
+ let mut read_buffer = BufReader::new(file);
+
+ loop {
+ let mut line = String::new();
+ let bytes_read = read_buffer.read_line(&mut line).await?;
+ if bytes_read == 0 {
+ break;
+ }
+
+ let mut items = line.splitn(3, ':');
+ let username = items.next();
+ let password = items.next();
+ let (Some(user), Some(pass)) = (username, password) else {
+ break;
+ };
+
+ if !pass.starts_with('*') {
+ password_hashes.push((user.to_string(), pass.to_string()));
+ log::debug!("loaded user {user}");
+ }
+ }
+ Ok(password_hashes.into_iter())
+ }
+}
+
+impl Transform for LinuxAuthenticator
+where
+ S: Service, Error = Error> + 'static,
+ S::Future: 'static,
+ B: MessageBody + 'static,
+{
+ type Response = ServiceResponse>;
+ type Error = Error;
+ type InitError = ();
+ type Transform = AuthenticationService;
+ type Future = Ready>;
+
+ fn new_transform(&self, service: S) -> Self::Future {
+ ready(Ok(AuthenticationService::new(
+ Rc::new(service),
+ self.context.clone(),
+ self.authentication_path,
+ )))
+ }
+}
diff --git a/bmcd/src/authentication/mod.rs b/bmcd/src/authentication/mod.rs
new file mode 100644
index 0000000..1d78965
--- /dev/null
+++ b/bmcd/src/authentication/mod.rs
@@ -0,0 +1,5 @@
+pub mod authentication_context;
+pub mod authentication_errors;
+pub mod authentication_service;
+pub mod linux_authenticator;
+pub mod passwd_validator;
diff --git a/bmcd/src/authentication/passwd_validator.rs b/bmcd/src/authentication/passwd_validator.rs
new file mode 100644
index 0000000..70887f2
--- /dev/null
+++ b/bmcd/src/authentication/passwd_validator.rs
@@ -0,0 +1,21 @@
+use super::authentication_errors::AuthenticationError;
+
+pub trait PasswordValidator {
+ fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError>;
+}
+
+pub struct UnixValidator {}
+
+impl PasswordValidator for UnixValidator {
+ fn validate(hash: &str, password: &str) -> Result<(), AuthenticationError> {
+ log::debug!(
+ "computed={}",
+ pwhash::unix::crypt(password, hash).unwrap_or_default()
+ );
+ if !pwhash::unix::verify(password, hash) {
+ Err(AuthenticationError::IncorrectCredentials)
+ } else {
+ Ok(())
+ }
+ }
+}
diff --git a/bmcd/src/main.rs b/bmcd/src/main.rs
index 1a073a2..88ad732 100644
--- a/bmcd/src/main.rs
+++ b/bmcd/src/main.rs
@@ -1,4 +1,8 @@
-use crate::{flash_service::FlashService, legacy::info_config};
+use crate::config::Config;
+use crate::{
+ authentication::linux_authenticator::LinuxAuthenticator, flash_service::FlashService,
+ legacy::info_config,
+};
use actix_files::Files;
use actix_web::{
http::{self, KeepAlive},
@@ -9,15 +13,21 @@ use actix_web::{
use anyhow::Context;
use clap::{command, value_parser, Arg};
use log::LevelFilter;
-use openssl::ssl::SslAcceptorBuilder;
-use std::path::{Path, PathBuf};
+use openssl::pkey::{PKey, Private};
+use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslMethod};
+use openssl::x509::X509;
+use std::fs::OpenOptions;
+use std::io::Read;
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
use tpi_rs::app::{bmc_application::BmcApplication, event_application::run_event_listener};
+pub mod authentication;
pub mod config;
mod flash_service;
mod into_legacy_response;
mod legacy;
-use crate::config::Config;
-use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
const HTTPS_PORT: u16 = 443;
const HTTP_PORT: u16 = 80;
@@ -25,24 +35,21 @@ const HTTP_PORT: u16 = 80;
#[actix_web::main]
async fn main() -> anyhow::Result<()> {
init_logger();
-
- let config = Config::try_from(config_path()).context("Error parsing config file")?;
- let tls = load_tls_configuration(&config.tls.private_key, &config.tls.certificate)?;
- let tls6 = load_tls_configuration(&config.tls.private_key, &config.tls.certificate)?;
-
+ let (tls, tls6) = load_config()?;
let bmc = Data::new(BmcApplication::new().await?);
run_event_listener(bmc.clone().into_inner())?;
let flash_service = Data::new(FlashService::new());
+ let authentication = Arc::new(LinuxAuthenticator::new("/api/bmc/authenticate").await?);
let run_server = HttpServer::new(move || {
App::new()
- // Shared state: BmcApplication instance
.app_data(bmc.clone())
.app_data(flash_service.clone())
- // Legacy API
- .configure(legacy::config)
// Enable logger
.wrap(middleware::Logger::default())
+ .wrap(authentication.clone())
+ // Legacy API
+ .configure(legacy::config)
// Serve a static tree of files of the web UI. Must be the last item.
.service(Files::new("/", "/mnt/var/www/").index_file("index.html"))
})
@@ -111,12 +118,34 @@ fn config_path() -> PathBuf {
.into()
}
-fn load_tls_configuration>(
+fn load_keys_from_pem>(
private_key: P,
certificate: P,
-) -> anyhow::Result {
- let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
- builder.set_private_key_file(private_key.as_ref(), SslFiletype::PEM)?;
- builder.set_certificate_chain_file(certificate.as_ref())?;
- Ok(builder)
+) -> anyhow::Result<(PKey, X509)> {
+ let mut pkey = Vec::new();
+ let mut cert = Vec::new();
+ OpenOptions::new()
+ .read(true)
+ .open(private_key)?
+ .read_to_end(&mut pkey)?;
+ OpenOptions::new()
+ .read(true)
+ .open(certificate)?
+ .read_to_end(&mut cert)?;
+
+ let rsa_key = PKey::private_key_from_pem(&pkey)?;
+ let x509 = X509::from_pem(&cert)?;
+ Ok((rsa_key, x509))
+}
+
+fn load_config() -> anyhow::Result<(SslAcceptorBuilder, SslAcceptorBuilder)> {
+ let config = Config::try_from(config_path()).context("Error parsing config file")?;
+ let (private_key, cert) = load_keys_from_pem(&config.tls.private_key, &config.tls.certificate)?;
+ let mut tls = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
+ tls.set_private_key(&private_key)?;
+ tls.set_certificate(&cert)?;
+ let mut tls6 = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
+ tls6.set_private_key(&private_key)?;
+ tls6.set_certificate(&cert)?;
+ Ok((tls, tls6))
}