Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP User Detail extension #171

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.6", optional = true }
unftp-auth-jsonfile = { version = "0.3.4", optional = true }
unftp-sbe-rooter = "0.2.1"
unftp-sbe-restrict = "0.1.2"
url = "2.5.0"

[target.'cfg(unix)'.dependencies]
unftp-auth-pam = { version = "0.2.5", 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))
}
}
45 changes: 42 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,39 @@ 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
#[allow(dead_code)]
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;
69 changes: 69 additions & 0 deletions src/infra/userdetail_http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! 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,
#[allow(dead_code)]
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(UserDetailError::Generic)?;

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
Loading