Skip to content

Commit

Permalink
Add HTTP User Detail extension
Browse files Browse the repository at this point in the history
  • Loading branch information
hannesdejager committed Mar 1, 2024
1 parent 16a0b65 commit 7233987
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 66 deletions.
17 changes: 9 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ unftp-auth-rest = { version = "0.2.4", optional = true }
unftp-auth-jsonfile = { version = "0.3.3", optional = true }
unftp-sbe-rooter = "0.2.0"
unftp-sbe-restrict = "0.1.1"
url = "2.5.0"

[target.'cfg(unix)'.dependencies]
unftp-auth-pam = { version = "0.2.4", optional = true }
Expand Down
9 changes: 9 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub const REDIS_PORT: &str = "log-redis-port";
pub const ROOT_DIR: &str = "root-dir";
pub const STORAGE_BACKEND_TYPE: &str = "sbe-type";
pub const USR_JSON_PATH: &str = "usr-json-path";
pub const USR_HTTP_URL: &str = "usr-http-url";
pub const VERBOSITY: &str = "verbosity";

#[derive(ArgEnum, Clone, Debug)]
Expand Down Expand Up @@ -502,6 +503,14 @@ pub(crate) fn clap_app(tmp_dir: &str) -> clap::Command {
.env("UNFTP_USR_JSON_PATH")
.takes_value(true),
)
.arg(
Arg::new(USR_HTTP_URL)
.long("usr-http-url")
.value_name("URL")
.help("The URL to fetch user details from via a GET request. The username will be appended to this path.")
.env("UNFTP_USR_HTTP_URL")
.takes_value(true),
)
.arg(
Arg::new(PUBSUB_BASE_URL)
.long("ntf-pubsub-base-url")
Expand Down
16 changes: 8 additions & 8 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::domain::user::{User, UserDetailProvider};
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
use async_trait::async_trait;
use libunftp::auth::{AuthenticationError, Credentials, DefaultUser};

Expand Down Expand Up @@ -32,11 +32,10 @@ impl libunftp::auth::Authenticator<User> for LookupAuthenticator {
) -> Result<User, AuthenticationError> {
self.inner.authenticate(username, creds).await?;
let user_provider = self.usr_detail.as_ref().unwrap();
if let Some(user) = user_provider.provide_user_detail(username) {
Ok(user)
} else {
Ok(User::with_defaults(username))
}
Ok(user_provider
.provide_user_detail(username)
.await
.map_err(|e| AuthenticationError::with_source("error getting user detail", e))?)
}

async fn cert_auth_sufficient(&self, username: &str) -> bool {
Expand All @@ -47,8 +46,9 @@ impl libunftp::auth::Authenticator<User> for LookupAuthenticator {
#[derive(Debug)]
pub struct DefaultUserProvider {}

#[async_trait]
impl UserDetailProvider for DefaultUserProvider {
fn provide_user_detail(&self, username: &str) -> Option<User> {
Some(User::with_defaults(username))
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
Ok(User::with_defaults(username))
}
}
44 changes: 41 additions & 3 deletions src/domain/user.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
//! Contains definitions pertaining to FTP User Detail
use async_trait::async_trait;
use libunftp::auth::UserDetail;
use std::fmt::{Debug, Display, Formatter};
use std::path::PathBuf;
use slog::error;
use std::{
fmt::{Debug, Display, Formatter},
path::PathBuf,
};
use thiserror::Error;
use unftp_sbe_restrict::{UserWithPermissions, VfsOperations};
use unftp_sbe_rooter::UserWithRoot;

Expand Down Expand Up @@ -64,6 +70,38 @@ impl UserWithPermissions for User {

/// Implementation of UserDetailProvider can look up and provide FTP user account details from
/// a source.
#[async_trait]
pub trait UserDetailProvider: Debug {
fn provide_user_detail(&self, username: &str) -> Option<User>;
/// This will do the lookup. An error is returned if the user was not found or something else
/// went wrong.
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError>;
}

/// The error type returned by [`UserDetailProvider`]
#[derive(Debug, Error)]
pub enum UserDetailError {
#[error("{0}")]
Generic(String),
#[error("user '{username:?}' not found")]
UserNotFound { username: String },
#[error("error getting user details: {0}: {1:?}")]
ImplPropagated(
String,
#[source] Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
),
}

impl UserDetailError {
/// Creates a new domain specific error
pub fn new(s: impl Into<String>) -> Self {
UserDetailError::ImplPropagated(s.into(), None)
}

/// Creates a new domain specific error with the given source error.
pub fn with_source<E>(s: impl Into<String>, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
UserDetailError::ImplPropagated(s.into(), Some(Box::new(source)))
}
}
4 changes: 2 additions & 2 deletions src/infra/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Infra contains infrastructure specific implementations of things in the [`domain`](crate::domain)
//! module.
mod pubsub;
mod workload_identity;

pub mod userdetail_http;
pub mod usrdetail_json;
mod workload_identity;

pub use pubsub::PubsubEventDispatcher;
68 changes: 68 additions & 0 deletions src/infra/userdetail_http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! A libunftp [`UserDetail`](libunftp::auth::user::UserDetail) provider that obtains user detail
//! over HTTP.
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
use crate::infra::usrdetail_json::JsonUserProvider;
use async_trait::async_trait;
use http::{Method, Request};
use hyper::{Body, Client};
use url::form_urlencoded;

/// A libunftp [`UserDetail`](libunftp::auth::user::UserDetail) provider that obtains user detail
/// over HTTP.
#[derive(Debug)]
pub struct HTTPUserDetailProvider {
url: String,
header_name: Option<String>,
}

impl HTTPUserDetailProvider {
/// Creates a provider that will obtain user detail from the specified URL.
pub fn new(url: impl Into<String>) -> HTTPUserDetailProvider {
HTTPUserDetailProvider {
url: url.into(),
header_name: None,
}
}
}

impl Default for HTTPUserDetailProvider {
fn default() -> Self {
HTTPUserDetailProvider {
url: "http://localhost:8080/users/".to_string(),
header_name: None,
}
}
}

#[async_trait]
impl UserDetailProvider for HTTPUserDetailProvider {
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
let url_suffix: String = form_urlencoded::byte_serialize(username.as_bytes()).collect();
let req = Request::builder()
.method(Method::GET)
.header("Content-type", "application/json")
.uri(format!("{}{}", self.url, username))
.body(Body::empty())
.map_err(|e| UserDetailError::with_source("error creating request", e))?;

let client = Client::new();

let resp = client
.request(req)
.await
.map_err(|e| UserDetailError::with_source("error doing HTTP request", e))?;

let body_bytes = hyper::body::to_bytes(resp.into_body())
.await
.map_err(|e| UserDetailError::with_source("error parsing body", e))?;

let json_str = std::str::from_utf8(body_bytes.as_ref())
.map_err(|e| UserDetailError::with_source("body is not a valid UTF string", e))?;

let json_usr_provider =
JsonUserProvider::from_json(json_str).map_err(|e| UserDetailError::Generic(e))?;

json_usr_provider.provide_user_detail(username).await
}
}
82 changes: 45 additions & 37 deletions src/infra/usrdetail_json.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::domain::user::{User, UserDetailProvider};
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use unftp_sbe_restrict::VfsOperations;
Expand Down Expand Up @@ -28,42 +29,49 @@ impl JsonUserProvider {
}
}

#[async_trait]
impl UserDetailProvider for JsonUserProvider {
fn provide_user_detail(&self, username: &str) -> Option<User> {
self.users.iter().find(|u| u.username == username).map(|u| {
let u = u.clone();
User {
username: u.username,
name: u.name,
surname: u.surname,
account_enabled: u.account_enabled.unwrap_or(true),
vfs_permissions: u.vfs_perms.map_or(VfsOperations::all(), |p| {
p.iter()
.fold(VfsOperations::all(), |ops, s| match s.as_str() {
"none" => VfsOperations::empty(),
"all" => VfsOperations::all(),
"-mkdir" => ops - VfsOperations::MK_DIR,
"-rmdir" => ops - VfsOperations::RM_DIR,
"-del" => ops - VfsOperations::DEL,
"-ren" => ops - VfsOperations::RENAME,
"-md5" => ops - VfsOperations::MD5,
"-get" => ops - VfsOperations::GET,
"-put" => ops - VfsOperations::PUT,
"-list" => ops - VfsOperations::LIST,
"+mkdir" => ops | VfsOperations::MK_DIR,
"+rmdir" => ops | VfsOperations::RM_DIR,
"+del" => ops | VfsOperations::DEL,
"+ren" => ops | VfsOperations::RENAME,
"+md5" => ops | VfsOperations::MD5,
"+get" => ops | VfsOperations::GET,
"+put" => ops | VfsOperations::PUT,
"+list" => ops | VfsOperations::LIST,
_ => ops,
})
}),
allowed_mime_types: None,
root: u.root.map(PathBuf::from),
}
})
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
self.users
.iter()
.find(|u| u.username == username)
.ok_or(UserDetailError::UserNotFound {
username: String::from(username),
})
.map(|u| {
let u = u.clone();
User {
username: u.username,
name: u.name,
surname: u.surname,
account_enabled: u.account_enabled.unwrap_or(true),
vfs_permissions: u.vfs_perms.map_or(VfsOperations::all(), |p| {
p.iter()
.fold(VfsOperations::all(), |ops, s| match s.as_str() {
"none" => VfsOperations::empty(),
"all" => VfsOperations::all(),
"-mkdir" => ops - VfsOperations::MK_DIR,
"-rmdir" => ops - VfsOperations::RM_DIR,
"-del" => ops - VfsOperations::DEL,
"-ren" => ops - VfsOperations::RENAME,
"-md5" => ops - VfsOperations::MD5,
"-get" => ops - VfsOperations::GET,
"-put" => ops - VfsOperations::PUT,
"-list" => ops - VfsOperations::LIST,
"+mkdir" => ops | VfsOperations::MK_DIR,
"+rmdir" => ops | VfsOperations::RM_DIR,
"+del" => ops | VfsOperations::DEL,
"+ren" => ops | VfsOperations::RENAME,
"+md5" => ops | VfsOperations::MD5,
"+get" => ops | VfsOperations::GET,
"+put" => ops | VfsOperations::PUT,
"+list" => ops | VfsOperations::LIST,
_ => ops,
})
}),
allowed_mime_types: None,
root: u.root.map(PathBuf::from),
}
})
}
}
30 changes: 22 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod metrics;
mod notify;
mod storage;

use crate::infra::userdetail_http::HTTPUserDetailProvider;
use crate::{
app::libunftp_version, args::FtpsClientAuthType, auth::DefaultUserProvider, notify::FTPListener,
};
Expand Down Expand Up @@ -99,14 +100,27 @@ fn make_auth(
Some("json") => make_json_auth(m),
unknown_type => Err(format!("unknown auth type: {}", unknown_type.unwrap())),
}?;
auth.set_usr_detail(match m.value_of(args::USR_JSON_PATH) {
Some(path) => {
let json: String = load_user_file(path)
.map_err(|e| format!("could not load user file '{}': {}", path, e))?;
Box::new(JsonUserProvider::from_json(json.as_str())?)
}
None => Box::new(DefaultUserProvider {}),
});
auth.set_usr_detail(
match (
m.value_of(args::USR_JSON_PATH),
m.value_of(args::USR_HTTP_URL),
) {
(Some(path), None) => {
let json: String = load_user_file(path)
.map_err(|e| format!("could not load user file '{}': {}", path, e))?;
Box::new(JsonUserProvider::from_json(json.as_str())?)
}
(None, Some(url)) => Box::new(HTTPUserDetailProvider::new(url)),
(None, None) => Box::new(DefaultUserProvider {}),
_ => {
return Err(format!(
"please specify either '{}' or '{}' but not both",
args::USR_JSON_PATH,
args::USR_HTTP_URL
))
}
},
);
Ok(Arc::new(auth))
}

Expand Down

0 comments on commit 7233987

Please sign in to comment.