Skip to content

Commit d6a91f4

Browse files
committed
Add HTTP User Detail extension
1 parent 16a0b65 commit d6a91f4

File tree

7 files changed

+180
-58
lines changed

7 files changed

+180
-58
lines changed

src/args.rs

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub const REDIS_PORT: &str = "log-redis-port";
4242
pub const ROOT_DIR: &str = "root-dir";
4343
pub const STORAGE_BACKEND_TYPE: &str = "sbe-type";
4444
pub const USR_JSON_PATH: &str = "usr-json-path";
45+
pub const USR_HTTP_URL: &str = "usr-http-url";
4546
pub const VERBOSITY: &str = "verbosity";
4647

4748
#[derive(ArgEnum, Clone, Debug)]
@@ -502,6 +503,14 @@ pub(crate) fn clap_app(tmp_dir: &str) -> clap::Command {
502503
.env("UNFTP_USR_JSON_PATH")
503504
.takes_value(true),
504505
)
506+
.arg(
507+
Arg::new(USR_HTTP_URL)
508+
.long("usr-http-url")
509+
.value_name("URL")
510+
.help("The URL to fetch user details from via a GET request")
511+
.env("UNFTP_USR_HTTP_URL")
512+
.takes_value(true),
513+
)
505514
.arg(
506515
Arg::new(PUBSUB_BASE_URL)
507516
.long("ntf-pubsub-base-url")

src/auth.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::domain::user::{User, UserDetailProvider};
1+
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
22
use async_trait::async_trait;
33
use libunftp::auth::{AuthenticationError, Credentials, DefaultUser};
44

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

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

49+
#[async_trait]
5050
impl UserDetailProvider for DefaultUserProvider {
51-
fn provide_user_detail(&self, username: &str) -> Option<User> {
52-
Some(User::with_defaults(username))
51+
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
52+
Ok(User::with_defaults(username))
5353
}
5454
}

src/domain/user.rs

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
//! Contains definitions pertaining to FTP User Detail
2+
use async_trait::async_trait;
13
use libunftp::auth::UserDetail;
2-
use std::fmt::{Debug, Display, Formatter};
3-
use std::path::PathBuf;
4+
use slog::error;
5+
use std::{
6+
fmt::{Debug, Display, Formatter},
7+
path::PathBuf,
8+
};
9+
use thiserror::Error;
410
use unftp_sbe_restrict::{UserWithPermissions, VfsOperations};
511
use unftp_sbe_rooter::UserWithRoot;
612

@@ -64,6 +70,38 @@ impl UserWithPermissions for User {
6470

6571
/// Implementation of UserDetailProvider can look up and provide FTP user account details from
6672
/// a source.
73+
#[async_trait]
6774
pub trait UserDetailProvider: Debug {
68-
fn provide_user_detail(&self, username: &str) -> Option<User>;
75+
/// This will do the lookup. An error is returned if the user was not found or something else
76+
/// went wrong.
77+
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError>;
78+
}
79+
80+
/// The error type returned by [`UserDetailProvider`]
81+
#[derive(Debug, Error)]
82+
pub enum UserDetailError {
83+
#[error("{0}")]
84+
Generic(String),
85+
#[error("user '{username:?}' not found")]
86+
UserNotFound { username: String },
87+
#[error("error getting user details: {0}: {1:?}")]
88+
ImplPropagated(
89+
String,
90+
#[source] Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
91+
),
92+
}
93+
94+
impl UserDetailError {
95+
/// Creates a new domain specific error
96+
pub fn new(s: impl Into<String>) -> Self {
97+
UserDetailError::ImplPropagated(s.into(), None)
98+
}
99+
100+
/// Creates a new domain specific error with the given source error.
101+
pub fn with_source<E>(s: impl Into<String>, source: E) -> Self
102+
where
103+
E: std::error::Error + Send + Sync + 'static,
104+
{
105+
UserDetailError::ImplPropagated(s.into(), Some(Box::new(source)))
106+
}
69107
}

src/infra/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Infra contains infrastructure specific implementations of things in the [`domain`](crate::domain)
22
//! module.
33
mod pubsub;
4-
mod workload_identity;
5-
4+
pub mod userdetail_http;
65
pub mod usrdetail_json;
6+
mod workload_identity;
77

88
pub use pubsub::PubsubEventDispatcher;

src/infra/userdetail_http.rs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! A libunftp [`UserDetail`](libunftp::auth::user::UserDetail) provider that obtains user detail
2+
//! over HTTP.
3+
4+
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
5+
use crate::infra::usrdetail_json::JsonUserProvider;
6+
use async_trait::async_trait;
7+
use http::{Method, Request};
8+
use hyper::{Body, Client};
9+
10+
/// A libunftp [`UserDetail`](libunftp::auth::user::UserDetail) provider that obtains user detail
11+
/// over HTTP.
12+
#[derive(Debug)]
13+
pub struct HTTPUserDetailProvider {
14+
url: String,
15+
}
16+
17+
impl HTTPUserDetailProvider {
18+
/// Creates a provider that will obtain user detail from the specified URL.
19+
pub fn new(url: impl Into<String>) -> HTTPUserDetailProvider {
20+
HTTPUserDetailProvider { url: url.into() }
21+
}
22+
}
23+
24+
#[async_trait]
25+
impl UserDetailProvider for HTTPUserDetailProvider {
26+
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
27+
let req = Request::builder()
28+
.method(Method::GET)
29+
.header("Content-type", "application/json")
30+
.uri(&self.url)
31+
.body(Body::empty())
32+
.map_err(|e| UserDetailError::with_source("error creating request", e))?;
33+
34+
let client = Client::new();
35+
36+
let resp = client
37+
.request(req)
38+
.await
39+
.map_err(|e| UserDetailError::with_source("error doing HTTP request", e))?;
40+
41+
let body_bytes = hyper::body::to_bytes(resp.into_body())
42+
.await
43+
.map_err(|e| UserDetailError::with_source("error parsing body", e))?;
44+
45+
let json_str = std::str::from_utf8(body_bytes.as_ref())
46+
.map_err(|e| UserDetailError::with_source("body is not a valid UTF string", e))?;
47+
48+
let json_usr_provider =
49+
JsonUserProvider::from_json(json_str).map_err(|e| UserDetailError::Generic(e))?;
50+
51+
json_usr_provider.provide_user_detail(username).await
52+
}
53+
}

src/infra/usrdetail_json.rs

+45-37
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::domain::user::{User, UserDetailProvider};
1+
use crate::domain::user::{User, UserDetailError, UserDetailProvider};
2+
use async_trait::async_trait;
23
use serde::Deserialize;
34
use std::path::PathBuf;
45
use unftp_sbe_restrict::VfsOperations;
@@ -28,42 +29,49 @@ impl JsonUserProvider {
2829
}
2930
}
3031

32+
#[async_trait]
3133
impl UserDetailProvider for JsonUserProvider {
32-
fn provide_user_detail(&self, username: &str) -> Option<User> {
33-
self.users.iter().find(|u| u.username == username).map(|u| {
34-
let u = u.clone();
35-
User {
36-
username: u.username,
37-
name: u.name,
38-
surname: u.surname,
39-
account_enabled: u.account_enabled.unwrap_or(true),
40-
vfs_permissions: u.vfs_perms.map_or(VfsOperations::all(), |p| {
41-
p.iter()
42-
.fold(VfsOperations::all(), |ops, s| match s.as_str() {
43-
"none" => VfsOperations::empty(),
44-
"all" => VfsOperations::all(),
45-
"-mkdir" => ops - VfsOperations::MK_DIR,
46-
"-rmdir" => ops - VfsOperations::RM_DIR,
47-
"-del" => ops - VfsOperations::DEL,
48-
"-ren" => ops - VfsOperations::RENAME,
49-
"-md5" => ops - VfsOperations::MD5,
50-
"-get" => ops - VfsOperations::GET,
51-
"-put" => ops - VfsOperations::PUT,
52-
"-list" => ops - VfsOperations::LIST,
53-
"+mkdir" => ops | VfsOperations::MK_DIR,
54-
"+rmdir" => ops | VfsOperations::RM_DIR,
55-
"+del" => ops | VfsOperations::DEL,
56-
"+ren" => ops | VfsOperations::RENAME,
57-
"+md5" => ops | VfsOperations::MD5,
58-
"+get" => ops | VfsOperations::GET,
59-
"+put" => ops | VfsOperations::PUT,
60-
"+list" => ops | VfsOperations::LIST,
61-
_ => ops,
62-
})
63-
}),
64-
allowed_mime_types: None,
65-
root: u.root.map(PathBuf::from),
66-
}
67-
})
34+
async fn provide_user_detail(&self, username: &str) -> Result<User, UserDetailError> {
35+
self.users
36+
.iter()
37+
.find(|u| u.username == username)
38+
.ok_or(UserDetailError::UserNotFound {
39+
username: String::from(username),
40+
})
41+
.map(|u| {
42+
let u = u.clone();
43+
User {
44+
username: u.username,
45+
name: u.name,
46+
surname: u.surname,
47+
account_enabled: u.account_enabled.unwrap_or(true),
48+
vfs_permissions: u.vfs_perms.map_or(VfsOperations::all(), |p| {
49+
p.iter()
50+
.fold(VfsOperations::all(), |ops, s| match s.as_str() {
51+
"none" => VfsOperations::empty(),
52+
"all" => VfsOperations::all(),
53+
"-mkdir" => ops - VfsOperations::MK_DIR,
54+
"-rmdir" => ops - VfsOperations::RM_DIR,
55+
"-del" => ops - VfsOperations::DEL,
56+
"-ren" => ops - VfsOperations::RENAME,
57+
"-md5" => ops - VfsOperations::MD5,
58+
"-get" => ops - VfsOperations::GET,
59+
"-put" => ops - VfsOperations::PUT,
60+
"-list" => ops - VfsOperations::LIST,
61+
"+mkdir" => ops | VfsOperations::MK_DIR,
62+
"+rmdir" => ops | VfsOperations::RM_DIR,
63+
"+del" => ops | VfsOperations::DEL,
64+
"+ren" => ops | VfsOperations::RENAME,
65+
"+md5" => ops | VfsOperations::MD5,
66+
"+get" => ops | VfsOperations::GET,
67+
"+put" => ops | VfsOperations::PUT,
68+
"+list" => ops | VfsOperations::LIST,
69+
_ => ops,
70+
})
71+
}),
72+
allowed_mime_types: None,
73+
root: u.root.map(PathBuf::from),
74+
}
75+
})
6876
}
6977
}

src/main.rs

+22-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod metrics;
1414
mod notify;
1515
mod storage;
1616

17+
use crate::infra::userdetail_http::HTTPUserDetailProvider;
1718
use crate::{
1819
app::libunftp_version, args::FtpsClientAuthType, auth::DefaultUserProvider, notify::FTPListener,
1920
};
@@ -99,14 +100,27 @@ fn make_auth(
99100
Some("json") => make_json_auth(m),
100101
unknown_type => Err(format!("unknown auth type: {}", unknown_type.unwrap())),
101102
}?;
102-
auth.set_usr_detail(match m.value_of(args::USR_JSON_PATH) {
103-
Some(path) => {
104-
let json: String = load_user_file(path)
105-
.map_err(|e| format!("could not load user file '{}': {}", path, e))?;
106-
Box::new(JsonUserProvider::from_json(json.as_str())?)
107-
}
108-
None => Box::new(DefaultUserProvider {}),
109-
});
103+
auth.set_usr_detail(
104+
match (
105+
m.value_of(args::USR_JSON_PATH),
106+
m.value_of(args::USR_HTTP_URL),
107+
) {
108+
(Some(path), None) => {
109+
let json: String = load_user_file(path)
110+
.map_err(|e| format!("could not load user file '{}': {}", path, e))?;
111+
Box::new(JsonUserProvider::from_json(json.as_str())?)
112+
}
113+
(None, Some(url)) => Box::new(HTTPUserDetailProvider::new(url)),
114+
(None, None) => Box::new(DefaultUserProvider {}),
115+
_ => {
116+
return Err(format!(
117+
"please specify either '{}' or '{}' but not both",
118+
args::USR_JSON_PATH,
119+
args::USR_HTTP_URL
120+
))
121+
}
122+
},
123+
);
110124
Ok(Arc::new(auth))
111125
}
112126

0 commit comments

Comments
 (0)