use std::{collections::HashMap, path::Path};
use super::Authenticator;
use crate::authz::Authorizable;
const HTTP_BASIC_PREFIX: &str = "Basic ";
#[derive(Clone, Debug)]
pub struct HttpBasic {
authmap: HashMap<String, String>,
}
impl HttpBasic {
pub async fn from_file(authfile: impl AsRef<Path>) -> anyhow::Result<Self> {
let raw = tokio::fs::read_to_string(&authfile).await?;
let mut authmap = HashMap::new();
for line in raw.split_terminator('\n') {
let line = line.trim();
let pair: Vec<&str> = line.splitn(2, ':').collect();
if pair.len() == 2 {
authmap.insert(pair[0].to_owned(), pair[1].to_owned());
}
}
Ok(HttpBasic { authmap })
}
fn check_credentials(&self, username: &str, password: &str) -> bool {
match self.authmap.get(username) {
Some(ciphertext) => {
if ciphertext.starts_with("$2y$") {
match bcrypt::verify(password, ciphertext) {
Err(e) => {
tracing::warn!(%e, "Error verifying bcrypted passwd");
false
}
Ok(res) => res,
}
} else {
tracing::warn!("htpasswd has entries in the wrong format.");
false
}
}
None => {
let _ = bcrypt::verify(
username,
"$2y$07$QCVM96JWmNWzx3k/7g1UXOLAO2y0imHGNjzEVkQoikrsV3gd4Xqk6",
);
false
}
}
}
}
#[async_trait::async_trait]
impl Authenticator for HttpBasic {
type Item = HttpUser;
async fn authenticate(&self, auth_data: &str) -> anyhow::Result<Self::Item> {
if auth_data.is_empty() {
anyhow::bail!("Username and password are required")
}
let (username, password) = parse_basic(auth_data)?;
match self.check_credentials(&username, &password) {
true => Ok(HttpUser { username }),
false => anyhow::bail!("Authentication failed"),
}
}
}
fn parse_basic(auth_data: &str) -> anyhow::Result<(String, String)> {
match auth_data.strip_prefix(HTTP_BASIC_PREFIX) {
None => anyhow::bail!("Wrong auth type. Only Basic auth is supported"),
Some(suffix) => {
let decoded = String::from_utf8(base64::decode(suffix)?)?;
let pair: Vec<&str> = decoded.splitn(2, ':').collect();
if pair.len() != 2 {
anyhow::bail!("Malformed Basic header")
} else {
Ok((pair[0].to_owned(), pair[1].to_owned()))
}
}
}
}
pub struct HttpUser {
username: String,
}
impl Authorizable for HttpUser {
fn principal(&self) -> String {
self.username.clone()
}
fn groups(&self) -> Vec<String> {
Vec::with_capacity(0)
}
}
#[cfg(test)]
mod test {
#[test]
fn test_parse_basic() {
let (name, pw) =
super::parse_basic("Basic YWRtaW46c3cwcmRmMXNo").expect("Basic header should parse");
assert_eq!("admin", name);
assert_eq!("sw0rdf1sh", pw, "the password is always swordfish");
super::parse_basic("NotBasic fadsfasdjkfhsadkjfhkashdfa").expect_err("Not a Basic header");
}
#[tokio::test]
async fn test_load_and_auth() {
let authfile = "test/data/htpasswd";
let basic = super::HttpBasic::from_file(authfile)
.await
.expect("File should load");
assert!(
basic.check_credentials("admin", "sw0rdf1sh"),
"The password is always swordfish"
);
assert!(
!basic.check_credentials("nope", "password"),
"should fail on nonexistent user"
);
assert!(
!basic.check_credentials("admin", "swordfish"),
"The password is not swordfish"
);
}
}