Skip to content

Commit 2b593c6

Browse files
committed
Split Authentication from User Detail Providing
1 parent 9f03f5f commit 2b593c6

File tree

21 files changed

+442
-135
lines changed

21 files changed

+442
-135
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
### Upcoming release
4+
5+
- **BREAKING**: Refactored `Authenticator` trait to be non-generic and return `Principal` instead of a generic `User` type. This decouples authentication (verifying credentials) from user detail retrieval (obtaining full user information).
6+
- Introduced `Principal` struct representing an authenticated user's identity (username). This is the minimal information returned by authentication.
7+
- Introduced `UserDetailProvider` trait to convert a `Principal` into a full `UserDetail` implementation. This allows authentication and user detail lookup to be separated.
8+
- Introduced `AuthenticationPipeline` struct that combines an `Authenticator` and a `UserDetailProvider` to provide a complete authentication flow.
9+
- Added `DefaultUserDetailProvider` implementation that returns `DefaultUser` for convenience.
10+
- **BREAKING**: Updated all `unftp-auth-*` crates (`unftp-auth-jsonfile`, `unftp-auth-pam`, `unftp-auth-rest`) to use the new non-generic `Authenticator` trait.
11+
- Updated all examples and tests to use the new authentication pattern.
12+
313
### libunftp 0.22.0
414

515
- Compile against Rust 1.92.0 in CI

crates/unftp-auth-jsonfile/src/lib.rs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ use bytes::Bytes;
150150
use flate2::read::GzDecoder;
151151
use ipnet::Ipv4Net;
152152
use iprange::IpRange;
153-
use libunftp::auth::{AuthenticationError, Authenticator, DefaultUser};
153+
use libunftp::auth::{AuthenticationError, Authenticator, Principal};
154154
use ring::{
155155
digest::SHA256_OUTPUT_LEN,
156156
pbkdf2::{PBKDF2_HMAC_SHA256, verify},
@@ -343,17 +343,19 @@ impl JsonFileAuthenticator {
343343
}
344344

345345
#[async_trait]
346-
impl Authenticator<DefaultUser> for JsonFileAuthenticator {
346+
impl Authenticator for JsonFileAuthenticator {
347347
#[tracing_attributes::instrument]
348-
async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<DefaultUser, AuthenticationError> {
348+
async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<Principal, AuthenticationError> {
349349
let res = if let Some(actual_creds) = self.credentials_map.get(username) {
350350
let client_cert = &actual_creds.client_cert;
351351
let certificate = &creds.certificate_chain.as_ref().and_then(|x| x.first());
352352

353353
let ip_check_result = if !Self::ip_ok(creds, actual_creds) {
354354
Err(AuthenticationError::IpDisallowed)
355355
} else {
356-
Ok(DefaultUser {})
356+
Ok(Principal {
357+
username: username.to_string(),
358+
})
357359
};
358360

359361
let cn_check_result = match (&client_cert, certificate) {
@@ -364,14 +366,18 @@ impl Authenticator<DefaultUser> for JsonFileAuthenticator {
364366
(Some(cn), cert) => match cert.verify_cn(cn) {
365367
Ok(is_authorized) => {
366368
if is_authorized {
367-
Some(Ok(DefaultUser {}))
369+
Some(Ok(Principal {
370+
username: username.to_string(),
371+
}))
368372
} else {
369373
Some(Err(AuthenticationError::CnDisallowed))
370374
}
371375
}
372376
Err(e) => Some(Err(AuthenticationError::with_source("verify_cn", e))),
373377
},
374-
(None, _) => Some(Ok(DefaultUser {})),
378+
(None, _) => Some(Ok(Principal {
379+
username: username.to_string(),
380+
})),
375381
},
376382
(Some(_), None) => Some(Err(AuthenticationError::CnDisallowed)),
377383
_ => None,
@@ -380,7 +386,9 @@ impl Authenticator<DefaultUser> for JsonFileAuthenticator {
380386
let pass_check_result = match &creds.password {
381387
Some(given_password) => {
382388
if Self::check_password(given_password, &actual_creds.password).is_ok() {
383-
Some(Ok(DefaultUser {}))
389+
Some(Ok(Principal {
390+
username: username.to_string(),
391+
}))
384392
} else {
385393
Some(Err(AuthenticationError::BadPassword))
386394
}
@@ -484,18 +492,23 @@ mod test {
484492
json_authenticator
485493
.authenticate("alice", &"this is the correct password for alice".into())
486494
.await
487-
.unwrap(),
488-
DefaultUser
495+
.unwrap()
496+
.username,
497+
"alice"
489498
);
490499
assert_eq!(
491500
json_authenticator
492501
.authenticate("bella", &"this is the correct password for bella".into())
493502
.await
494-
.unwrap(),
495-
DefaultUser
503+
.unwrap()
504+
.username,
505+
"bella"
496506
);
497-
assert_eq!(json_authenticator.authenticate("carol", &"not so secure".into()).await.unwrap(), DefaultUser);
498-
assert_eq!(json_authenticator.authenticate("dan", &"".into()).await.unwrap(), DefaultUser);
507+
assert_eq!(
508+
json_authenticator.authenticate("carol", &"not so secure".into()).await.unwrap().username,
509+
"carol"
510+
);
511+
assert_eq!(json_authenticator.authenticate("dan", &"".into()).await.unwrap().username, "dan");
499512
assert!(matches!(
500513
json_authenticator.authenticate("carol", &"this is the wrong password".into()).await,
501514
Err(AuthenticationError::BadPassword)
@@ -520,8 +533,9 @@ mod test {
520533
},
521534
)
522535
.await
523-
.unwrap(),
524-
DefaultUser
536+
.unwrap()
537+
.username,
538+
"dan"
525539
);
526540

527541
match json_authenticator
@@ -679,8 +693,9 @@ mod test {
679693
},
680694
)
681695
.await
682-
.unwrap(),
683-
DefaultUser
696+
.unwrap()
697+
.username,
698+
"alice"
684699
);
685700

686701
// correct password but missing certificate fails
@@ -711,8 +726,9 @@ mod test {
711726
},
712727
)
713728
.await
714-
.unwrap(),
715-
DefaultUser
729+
.unwrap()
730+
.username,
731+
"bob"
716732
);
717733

718734
// certificate with incorrect CN and no password needed according to json file fails to authenticate
@@ -743,8 +759,9 @@ mod test {
743759
},
744760
)
745761
.await
746-
.unwrap(),
747-
DefaultUser
762+
.unwrap()
763+
.username,
764+
"dean"
748765
);
749766
}
750767
}

crates/unftp-auth-jsonfile/tests/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![allow(missing_docs)]
22

3-
use libunftp::auth::{Authenticator, DefaultUser};
3+
use libunftp::auth::Authenticator;
44
use std::path::PathBuf;
55
use unftp_auth_jsonfile::JsonFileAuthenticator;
66

@@ -17,21 +17,21 @@ async fn credentials_from_file_type_plain() {
1717
let path = input_file_path("cred.json".to_string());
1818

1919
let json_auther = JsonFileAuthenticator::from_file(path).unwrap();
20-
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap(), DefaultUser);
20+
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap().username, "testuser");
2121
}
2222

2323
#[tokio::test(flavor = "current_thread")]
2424
async fn credentials_from_file_type_gzipped() {
2525
let path = input_file_path("cred.json.gz".to_string());
2626

2727
let json_auther = JsonFileAuthenticator::from_file(path).unwrap();
28-
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap(), DefaultUser);
28+
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap().username, "testuser");
2929
}
3030

3131
#[tokio::test(flavor = "current_thread")]
3232
async fn credentials_from_file_type_gzipped_base64() {
3333
let path = input_file_path("cred.json.gz.b64".to_string());
3434

3535
let json_auther = JsonFileAuthenticator::from_file(path).unwrap();
36-
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap(), DefaultUser);
36+
assert_eq!(json_auther.authenticate("testuser", &"testpassword".into()).await.unwrap().username, "testuser");
3737
}

crates/unftp-auth-pam/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! [`PAM`]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
77
88
use async_trait::async_trait;
9-
use libunftp::auth::*;
9+
use libunftp::auth::{AuthenticationError, Authenticator, Credentials, Principal};
1010

1111
/// [`Authenticator`] implementation that authenticates against [`PAM`].
1212
///
@@ -26,10 +26,10 @@ impl PamAuthenticator {
2626
}
2727

2828
#[async_trait]
29-
impl Authenticator<DefaultUser> for PamAuthenticator {
29+
impl Authenticator for PamAuthenticator {
3030
#[allow(clippy::type_complexity)]
3131
#[tracing_attributes::instrument]
32-
async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<DefaultUser, AuthenticationError> {
32+
async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<Principal, AuthenticationError> {
3333
let username = username.to_string();
3434
let password = creds.password.as_ref().ok_or(AuthenticationError::BadPassword)?;
3535
let service = self.service.clone();
@@ -38,6 +38,6 @@ impl Authenticator<DefaultUser> for PamAuthenticator {
3838

3939
auth.get_handler().set_credentials(&username, password);
4040
auth.authenticate().map_err(|e| AuthenticationError::with_source("pam error", e))?;
41-
Ok(DefaultUser {})
41+
Ok(Principal { username })
4242
}
4343
}

crates/unftp-auth-rest/src/lib.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use http_body_util::BodyExt;
77
use hyper::{Method, Request, http::uri::InvalidUri};
88
use hyper_util::client::legacy::Client;
99
use hyper_util::rt::TokioExecutor;
10-
use libunftp::auth::{AuthenticationError, Authenticator, Credentials, DefaultUser};
10+
use libunftp::auth::{AuthenticationError, Authenticator, Credentials, Principal};
1111
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
1212
use regex::Regex;
1313
use serde_json::{Value, json};
@@ -222,9 +222,9 @@ impl TrimQuotes for String {
222222
}
223223

224224
#[async_trait]
225-
impl Authenticator<DefaultUser> for RestAuthenticator {
225+
impl Authenticator for RestAuthenticator {
226226
#[tracing_attributes::instrument]
227-
async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<DefaultUser, AuthenticationError> {
227+
async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<Principal, AuthenticationError> {
228228
let username_url = utf8_percent_encode(username, NON_ALPHANUMERIC).collect::<String>();
229229
let password = creds.password.as_ref().ok_or(AuthenticationError::BadPassword)?.as_ref();
230230
let password_url = utf8_percent_encode(password, NON_ALPHANUMERIC).collect::<String>();
@@ -284,7 +284,9 @@ impl Authenticator<DefaultUser> for RestAuthenticator {
284284
};
285285

286286
if self.regex.is_match(&parsed) {
287-
Ok(DefaultUser {})
287+
Ok(Principal {
288+
username: username.to_string(),
289+
})
288290
} else {
289291
Err(AuthenticationError::BadPassword)
290292
}

crates/unftp-sbe-fs/examples/cap-ftpd-worker.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod auth {
2323
};
2424

2525
use async_trait::async_trait;
26-
use libunftp::auth::{AuthenticationError, Authenticator, DefaultUser, UserDetail};
26+
use libunftp::auth::{AuthenticationError, Authenticator, Principal, UserDetail, UserDetailError, UserDetailProvider};
2727
use serde::Deserialize;
2828
use tokio::time::sleep;
2929

@@ -114,14 +114,14 @@ mod auth {
114114
}
115115

116116
#[async_trait]
117-
impl Authenticator<User> for JsonFileAuthenticator {
117+
impl Authenticator for JsonFileAuthenticator {
118118
#[tracing_attributes::instrument]
119-
async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<User, AuthenticationError> {
119+
async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<Principal, AuthenticationError> {
120120
let res = if let Some(actual_creds) = self.credentials_map.get(username) {
121121
let pass_check_result = match &creds.password {
122122
Some(given_password) => {
123123
if Self::check_password(given_password, &actual_creds.password).is_ok() {
124-
Some(Ok(User::new(username, &actual_creds.home)))
124+
Some(Ok(()))
125125
} else {
126126
Some(Err(AuthenticationError::BadPassword))
127127
}
@@ -133,9 +133,11 @@ mod auth {
133133
None => Err(AuthenticationError::BadPassword),
134134
Some(pass_res) => {
135135
if pass_res.is_ok() {
136-
Ok(User::new(username, &actual_creds.home))
136+
Ok(Principal {
137+
username: username.to_string(),
138+
})
137139
} else {
138-
pass_res
140+
Err(AuthenticationError::BadPassword)
139141
}
140142
}
141143
}
@@ -156,11 +158,17 @@ mod auth {
156158
}
157159

158160
#[async_trait]
159-
impl Authenticator<DefaultUser> for JsonFileAuthenticator {
160-
#[tracing_attributes::instrument]
161-
async fn authenticate(&self, username: &str, creds: &libunftp::auth::Credentials) -> Result<DefaultUser, AuthenticationError> {
162-
let _: User = self.authenticate(username, creds).await?;
163-
Ok(DefaultUser {})
161+
impl UserDetailProvider for JsonFileAuthenticator {
162+
type User = User;
163+
164+
async fn provide_user_detail(&self, principal: &Principal) -> Result<User, UserDetailError> {
165+
if let Some(creds) = self.credentials_map.get(&principal.username) {
166+
Ok(User::new(&principal.username, &creds.home))
167+
} else {
168+
Err(UserDetailError::UserNotFound {
169+
username: principal.username.clone(),
170+
})
171+
}
164172
}
165173
}
166174
}
@@ -234,13 +242,14 @@ async fn main() {
234242

235243
let control_sock = TcpStream::from_std(std_stream).unwrap();
236244

237-
let auth = Arc::new(JsonFileAuthenticator::from_file(args[1].clone()).unwrap());
245+
let authenticator = Arc::new(JsonFileAuthenticator::from_file(args[1].clone()).unwrap());
246+
let user_provider = authenticator.clone();
238247
// XXX This would be a lot easier if the libunftp API allowed creating the
239248
// storage just before calling service.
240249
let storage = Mutex::new(Some(Filesystem::new(std::env::temp_dir()).unwrap()));
241250
let sgen = Box::new(move || storage.lock().unwrap().take().unwrap());
242251

243-
let mut sb = libunftp::ServerBuilder::with_authenticator(sgen, auth);
252+
let mut sb = libunftp::ServerBuilder::with_authenticator(sgen, authenticator).user_detail_provider(user_provider);
244253
cfg_if! {
245254
if #[cfg(target_os = "freebsd")] {
246255
// Safe because we're single-threaded

crates/unftp-sbe-gcs/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@
3737
//! constructors of `Server` e.g.
3838
//!
3939
//! ```no_run
40-
//! use libunftp::Server;
40+
//! use libunftp::ServerBuilder;
4141
//! use unftp_sbe_gcs::{CloudStorage, options::AuthMethod};
4242
//! use std::path::PathBuf;
43+
//! use std::sync::Arc;
4344
//!
4445
//! #[tokio::main]
4546
//! pub async fn main() {
46-
//! let server = libunftp::Server::new(
47+
//! let server = libunftp::ServerBuilder::new(
4748
//! Box::new(move || CloudStorage::with_bucket_root("my-bucket", PathBuf::from("/ftp-root"), AuthMethod::WorkloadIdentity(None)))
4849
//! )
4950
//! .greeting("Welcome to my FTP server")

src/auth/anonymous.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,24 @@ use async_trait::async_trait;
1111
/// ```rust
1212
/// # #[tokio::main]
1313
/// # async fn main() {
14-
/// use libunftp::auth::{Authenticator, AnonymousAuthenticator, DefaultUser};
14+
/// use libunftp::auth::{Authenticator, AnonymousAuthenticator, Principal};
1515
///
1616
/// let my_auth = AnonymousAuthenticator{};
17-
/// assert_eq!(my_auth.authenticate("Finn", &"I ❤️ PB".into()).await.unwrap(), DefaultUser{});
17+
/// assert_eq!(my_auth.authenticate("Finn", &"I ❤️ PB".into()).await.unwrap().username, "Finn");
1818
/// # }
1919
/// ```
2020
///
2121
#[derive(Debug)]
2222
pub struct AnonymousAuthenticator;
2323

2424
#[async_trait]
25-
impl Authenticator<DefaultUser> for AnonymousAuthenticator {
25+
impl Authenticator for AnonymousAuthenticator {
2626
#[allow(clippy::type_complexity)]
2727
#[tracing_attributes::instrument]
28-
async fn authenticate(&self, _username: &str, _password: &Credentials) -> Result<DefaultUser, AuthenticationError> {
29-
Ok(DefaultUser {})
28+
async fn authenticate(&self, username: &str, _password: &Credentials) -> Result<Principal, AuthenticationError> {
29+
Ok(Principal {
30+
username: username.to_string(),
31+
})
3032
}
3133

3234
async fn cert_auth_sufficient(&self, _username: &str) -> bool {

0 commit comments

Comments
 (0)