Skip to content

Commit

Permalink
make 'pam' and 'fsquota' optional features
Browse files Browse the repository at this point in the history
  • Loading branch information
miquels committed Nov 24, 2020
1 parent b20f569 commit a536270
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 124 deletions.
30 changes: 28 additions & 2 deletions Cargo.toml
Expand Up @@ -19,12 +19,38 @@ categories = ["filesystem"]

[workspace]

[features]
# By default, the "pam" and "quota" features are enabled.
#
# Some systems do not have pam (like OpenBSD), so to compile this
# package without pam but with quota use:
#
# cargo build --release --no-default-features --features=quota
#
default = [ "pam", "quota" ]

# dependencies for the feature.
pam = [ "pam-sandboxed" ]
quota = [ "fs-quota" ]

# Include debug info in release builds.
[profile.release]
debug = true

# Build dependencies in optimized mode, even for debug builds.
[profile.dev.package."*"]
opt-level = 3

# Build dev-dependencies in non-optimized mode, even for release builds.
[profile.dev.build-override]
opt-level = 0

[dependencies]
clap = "2.33.0"
enum_from_str = "0.1.0"
enum_from_str_derive = "0.1.0"
env_logger = "0.7.1"
fs-quota = { path = "fs_quota", version = "0.1.0" }
fs-quota = { path = "fs_quota", version = "0.1.0", optional = true }
futures = "0.3"
handlebars = "2.0.2"
headers = "0.3.0"
Expand All @@ -34,7 +60,7 @@ lazy_static = "1.4.0"
libc = "0.2.65"
log = "0.4.8"
net2 = "0.2.33"
pam-sandboxed = { path = "pam", version = "0.2.0" }
pam-sandboxed = { path = "pam", version = "0.2.0", optional = true }
percent-encoding = "2.1.0"
regex = "1.3.1"
serde = { version = "1.0.102", features = ["derive"] }
Expand Down
130 changes: 130 additions & 0 deletions src/auth.rs
@@ -0,0 +1,130 @@
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;

use crate::config::{AuthType, Config, Location};

use headers::{authorization::Basic, Authorization, HeaderMapExt};
use http::status::StatusCode;

type HttpRequest = http::Request<hyper::Body>;

#[derive(Clone)]
pub struct Auth {
config: Arc<Config>,
#[cfg(feature = "pam")]
pam_auth: pam_sandboxed::PamAuth,
}

impl Auth {

pub fn new(config: Arc<Config>) -> io::Result<Auth> {

// initialize pam.
#[cfg(feature = "pam")]
let pam_auth = {
// set cache timeouts.
if let Some(timeout) = config.pam.cache_timeout {
cache::cached::set_pamcache_timeout(timeout);
}
pam_sandboxed::PamAuth::new(config.pam.threads.clone())?
};

Ok(Auth {
#[cfg(feature = "pam")]
pam_auth,
config,
})
}

// authenticate user.
pub async fn auth<'a>(&'a self, req: &'a HttpRequest, location: &Location, _remote_ip: SocketAddr) -> Result<String, StatusCode> {
// we must have a login/pass
let basic = match req.headers().typed_get::<Authorization<Basic>>() {
Some(Authorization(basic)) => basic,
_ => return Err(StatusCode::UNAUTHORIZED),
};
let user = basic.username();
let pass = basic.password();

// match the auth type.
let auth_type = location.accounts.auth_type.as_ref().or(self.config.accounts.auth_type.as_ref());
match auth_type {
#[cfg(feature = "pam")]
Some(&AuthType::Pam) => self.auth_pam(req, user, pass, _remote_ip).await,
Some(&AuthType::HtPasswd(ref ht)) => self.auth_htpasswd(user, pass, ht.as_str()).await,
None => {
debug!("need authentication, but auth-type is not set");
Err(StatusCode::UNAUTHORIZED)
},
}
}

// authenticate user using PAM.
#[cfg(feature = "pam")]
async fn auth_pam<'a>(&'a self, req: &'a HttpRequest, user: &'a str, pass: &'a str, remote_ip: SocketAddr) -> Result<String, StatusCode> {
// stringify the remote IP address.
let ip = remote_ip.ip();
let ip_string = if ip.is_loopback() {
// if it's loopback, take the value from the x-forwarded-for
// header, if present.
req.headers()
.get("x-forwarded-for")
.and_then(|s| s.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_owned())
} else {
Some(match ip {
IpAddr::V4(ip) => ip.to_string(),
IpAddr::V6(ip) => ip.to_string(),
})
};
let ip_ref = ip_string.as_ref().map(|s| s.as_str());

// authenticate.
let service = self.config.pam.service.as_str();
let pam_auth = self.pam_auth.clone();
match cache::cached::pam_auth(pam_auth, service, user, pass, ip_ref).await {
Ok(_) => Ok(user.to_string()),
Err(_) => {
debug!("auth_pam({}): authentication for {} ({:?}) failed", service, user, ip_ref);
Err(StatusCode::UNAUTHORIZED)
},
}
}

// authenticate user using htpasswd.
async fn auth_htpasswd<'a>(&'a self, user: &'a str, pass: &'a str, section: &'a str) -> Result<String, StatusCode> {

// Get the htpasswd.WHATEVER section from the config file.
let file = match self.config.htpasswd.get(section) {
Some(section) => section.htpasswd.as_str(),
None => return Err(StatusCode::UNAUTHORIZED),
};

// Read the file and split it into a bunch of lines.
tokio::task::block_in_place(move || {
let data = match std::fs::read_to_string(file) {
Ok(data) => data,
Err(e) => {
debug!("{}: {}", file, e);
return Err(StatusCode::UNAUTHORIZED);
},
};
let lines = data.split('\n').map(|s| s.trim()).filter(|s| !s.starts_with("#") && !s.is_empty());

// Check each line for a match.
for line in lines {
let mut fields = line.split(':');
if let (Some(htuser), Some(htpass)) = (fields.next(), fields.next()) {
if htuser == user && pwhash::unix::verify(pass, htpass) {
return Ok(user.to_string());
}
}
}

debug!("auth_htpasswd: authentication for {} failed", user);
Err(StatusCode::UNAUTHORIZED)
})
}
}
8 changes: 5 additions & 3 deletions src/cache.rs
Expand Up @@ -95,16 +95,13 @@ pub(crate) mod cached {
//
// Cached versions of Unix account lookup and Pam auth.
//
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use crate::cache;
use crate::unixuser::{self, User};
use lazy_static::lazy_static;
use pam_sandboxed::{PamAuth, PamError};

struct Timeouts {
pwcache: Duration,
Expand Down Expand Up @@ -140,6 +137,7 @@ pub(crate) mod cached {
timeouts.pamcache = Duration::new(secs as u64, 0);
}

#[cfg(feature = "pam")]
pub async fn pam_auth<'a>(
pam_auth: PamAuth,
service: &'a str,
Expand All @@ -148,6 +146,10 @@ pub(crate) mod cached {
remip: Option<&'a str>,
) -> Result<(), PamError>
{
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use pam_sandboxed::{PamAuth, PamError};

let mut s = DefaultHasher::new();
service.hash(&mut s);
user.hash(&mut s);
Expand Down
18 changes: 11 additions & 7 deletions src/config.rs
Expand Up @@ -124,6 +124,7 @@ pub enum Auth {

#[derive(Debug, Clone)]
pub enum AuthType {
#[cfg(feature = "pam")]
Pam,
HtPasswd(String),
}
Expand Down Expand Up @@ -202,14 +203,16 @@ pub fn deserialize_authtype<'de, D>(deserializer: D) -> Result<Option<AuthType>,
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?;
if s.starts_with("htpasswd.") {
Ok(Some(AuthType::HtPasswd(s[9..].to_string())))
} else if &s == "pam" {
Ok(Some(AuthType::Pam))
} else if s == "" {
Ok(None)
} else {
Err(serde::de::Error::custom("unknown auth-type"))
return Ok(Some(AuthType::HtPasswd(s[9..].to_string())));
}
#[cfg(feature = "pam")]
if &s == "pam" {
return Ok(Some(AuthType::Pam));
}
if s == "" {
return Ok(None);
}
Err(serde::de::Error::custom("unknown auth-type"))
}

pub fn deserialize_opt_enum<'de, D, E>(deserializer: D) -> Result<Option<E>, D::Error>
Expand Down Expand Up @@ -265,6 +268,7 @@ pub fn build_routes(cfg: &str, config: &mut Config) -> io::Result<()> {
}

pub fn check(cfg: &str, config: &Config) {
#[cfg(feature = "pam")]
if let Some(AuthType::Pam) = config.accounts.auth_type {
if config.pam.service == "" {
eprintln!("{}: missing section [pam]", cfg);
Expand Down

0 comments on commit a536270

Please sign in to comment.