diff --git a/Cargo.toml b/Cargo.toml index 536671b2..3245d0cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,8 @@ tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } unftp-sbe-fs = { path = "../libunftp/crates/unftp-sbe-fs"} [patch.crates-io] -capsicum = { git = "https://github.com/asomers/capsicum-rs", rev = "24330ee"} -casper-sys = { git = "https://github.com/asomers/capsicum-rs", rev = "24330ee"} +capsicum = { git = "https://github.com/asomers/capsicum-rs", rev = "2feefa0"} +casper-sys = { git = "https://github.com/asomers/capsicum-rs", rev = "2feefa0"} [lints] -workspace=true \ No newline at end of file +workspace=true diff --git a/crates/unftp-sbe-fs/Cargo.toml b/crates/unftp-sbe-fs/Cargo.toml index 407f24fc..89c594a1 100644 --- a/crates/unftp-sbe-fs/Cargo.toml +++ b/crates/unftp-sbe-fs/Cargo.toml @@ -47,6 +47,9 @@ tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.18" getrandom = "0.2.12" +[target.'cfg(target_os = "freebsd")'.dependencies] +capsicum = { version = "0.3.0", features = [] } + [target.'cfg(target_os = "freebsd")'.dev-dependencies] capsicum = { version = "0.3.0", features = ["casper"] } capsicum-net = { version = "0.1.0", features = ["tokio"], git = "https://github.com/asomers/capsicum-net", rev = "c6fc574" } diff --git a/crates/unftp-sbe-fs/examples/cap-ftpd-worker.rs b/crates/unftp-sbe-fs/examples/cap-ftpd-worker.rs index f26cadb8..e005700d 100644 --- a/crates/unftp-sbe-fs/examples/cap-ftpd-worker.rs +++ b/crates/unftp-sbe-fs/examples/cap-ftpd-worker.rs @@ -31,6 +31,7 @@ mod auth { pub struct User { username: String, home: Option, + read_only: bool, } #[derive(Deserialize, Clone, Debug)] @@ -40,6 +41,8 @@ mod auth { username: String, password: Option, home: Option, + #[serde(default)] + read_only: bool, }, } @@ -47,13 +50,15 @@ mod auth { struct UserCreds { pub password: Option, pub home: Option, + pub read_only: bool, } impl User { - fn new(username: &str, home: &Option) -> Self { + fn new(username: &str, home: &Option, read_only: bool) -> Self { User { username: username.to_owned(), home: home.clone(), + read_only, } } } @@ -65,6 +70,10 @@ mod auth { Some(p) => Some(p.as_path()), } } + + fn read_only(&self) -> bool { + self.read_only + } } impl fmt::Display for User { @@ -99,7 +108,12 @@ mod auth { fn list_entry_to_map_entry(user_info: Credentials) -> Result<(String, UserCreds), Box> { let map_entry = match user_info { - Credentials::Plaintext { username, password, home } => (username.clone(), UserCreds { password, home }), + Credentials::Plaintext { + username, + password, + home, + read_only, + } => (username.clone(), UserCreds { password, home, read_only }), }; Ok(map_entry) } @@ -125,7 +139,7 @@ mod auth { let pass_check_result = match &creds.password { Some(ref given_password) => { if Self::check_password(given_password, &actual_creds.password).is_ok() { - Some(Ok(User::new(username, &actual_creds.home))) + Some(Ok(User::new(username, &actual_creds.home, actual_creds.read_only))) } else { Some(Err(AuthenticationError::BadPassword)) } @@ -137,7 +151,7 @@ mod auth { None => Err(AuthenticationError::BadPassword), Some(pass_res) => { if pass_res.is_ok() { - Ok(User::new(username, &actual_creds.home)) + Ok(User::new(username, &actual_creds.home, actual_creds.read_only)) } else { pass_res } diff --git a/crates/unftp-sbe-fs/src/lib.rs b/crates/unftp-sbe-fs/src/lib.rs index 041db946..8648e625 100644 --- a/crates/unftp-sbe-fs/src/lib.rs +++ b/crates/unftp-sbe-fs/src/lib.rs @@ -100,6 +100,28 @@ impl StorageBackend for Filesystem { }; self.root_fd = Arc::new(self.root_fd.open_dir(relpath)?); } + cfg_if! { + if #[cfg(target_os = "freebsd")] { + use capsicum::CapRights; + + let mut rights = capsicum::RightsBuilder::new(); + rights.allow(capsicum::Right::Fcntl); + rights.allow(capsicum::Right::Fstatat); + rights.allow(capsicum::Right::Lookup); + rights.allow(capsicum::Right::Read); + rights.allow(capsicum::Right::Seek); + if !user_detail.read_only() { + rights.allow(capsicum::Right::Create); + rights.allow(capsicum::Right::Ftruncate); + rights.allow(capsicum::Right::Mkdirat); + rights.allow(capsicum::Right::RenameatSource); + rights.allow(capsicum::Right::RenameatTarget); + rights.allow(capsicum::Right::Unlinkat); + rights.allow(capsicum::Right::Write); + } + rights.finalize().limit(&self.root_fd)?; + } + } Ok(()) } @@ -153,12 +175,15 @@ impl StorageBackend for Filesystem { async fn put + Send, R: tokio::io::AsyncRead + Send + Sync + 'static + Unpin>( &self, - _user: &User, + user: &User, bytes: R, path: P, start_pos: u64, ) -> Result { // TODO: Add permission checks + if user.read_only() { + return Err(Error::new(ErrorKind::PermissionDenied, "Write access denied")); + } let path = strip_prefixes(path.as_ref()); let mut oo = cap_std::fs::OpenOptions::new(); @@ -176,7 +201,11 @@ impl StorageBackend for Filesystem { } #[tracing_attributes::instrument] - async fn del + Send + Debug>(&self, _user: &User, path: P) -> Result<()> { + async fn del + Send + Debug>(&self, user: &User, path: P) -> Result<()> { + if user.read_only() { + return Err(Error::new(ErrorKind::PermissionDenied, "Write access denied")); + } + let path = strip_prefixes(path.as_ref()); cap_fs::remove_file(self.root_fd.clone(), path) .await @@ -184,7 +213,11 @@ impl StorageBackend for Filesystem { } #[tracing_attributes::instrument] - async fn rmd + Send + Debug>(&self, _user: &User, path: P) -> Result<()> { + async fn rmd + Send + Debug>(&self, user: &User, path: P) -> Result<()> { + if user.read_only() { + return Err(Error::new(ErrorKind::PermissionDenied, "Write access denied")); + } + let path = strip_prefixes(path.as_ref()); cap_fs::remove_dir(self.root_fd.clone(), path) .await @@ -192,7 +225,11 @@ impl StorageBackend for Filesystem { } #[tracing_attributes::instrument] - async fn mkd + Send + Debug>(&self, _user: &User, path: P) -> Result<()> { + async fn mkd + Send + Debug>(&self, user: &User, path: P) -> Result<()> { + if user.read_only() { + return Err(Error::new(ErrorKind::PermissionDenied, "Write access denied")); + } + let path = strip_prefixes(path.as_ref()); cap_fs::create_dir(self.root_fd.clone(), path) .await @@ -200,7 +237,11 @@ impl StorageBackend for Filesystem { } #[tracing_attributes::instrument] - async fn rename + Send + Debug>(&self, _user: &User, from: P, to: P) -> Result<()> { + async fn rename + Send + Debug>(&self, user: &User, from: P, to: P) -> Result<()> { + if user.read_only() { + return Err(Error::new(ErrorKind::PermissionDenied, "Write access denied")); + } + let from = from.as_ref().strip_prefix("/").unwrap_or(from.as_ref()); let to = to.as_ref().strip_prefix("/").unwrap_or(to.as_ref()); diff --git a/src/auth/user.rs b/src/auth/user.rs index b6da5f91..1529ab32 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -27,6 +27,11 @@ pub trait UserDetail: Send + Sync + Display + Debug { fn home(&self) -> Option<&Path> { None } + + /// Should the user have read-only access, regardless of the Unix file permissions? + fn read_only(&self) -> bool { + false + } } /// DefaultUser is a default implementation of the `UserDetail` trait that doesn't hold any user