diff --git a/Cargo.lock b/Cargo.lock index 216fa4c..b1c7002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2650,6 +2650,7 @@ dependencies = [ "unftp-auth-rest", "unftp-sbe-fs", "unftp-sbe-gcs", + "unftp-sbe-rooter", ] [[package]] @@ -2749,6 +2750,17 @@ dependencies = [ "yup-oauth2", ] +[[package]] +name = "unftp-sbe-rooter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac2bc638adf879204d2809b8fda34ebe2592bd80ca6ff149aa77be779568676" +dependencies = [ + "async-trait", + "libunftp", + "tokio", +] + [[package]] name = "unicase" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index a46b59c..3d46606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ unftp-sbe-fs = "0.2.2" unftp-sbe-gcs = { version = "0.2.3", optional = true } unftp-auth-rest = { version = "0.2.2", optional = true } unftp-auth-jsonfile = { version = "0.3.1", optional = true } +unftp-sbe-rooter = "0.1.0" # Purely to resolve conflicts tracing= "0.1.37" diff --git a/docs/libunftp/README.md b/docs/libunftp/README.md index 0c15c51..d70df60 100644 --- a/docs/libunftp/README.md +++ b/docs/libunftp/README.md @@ -34,6 +34,7 @@ Known storage back-ends: * [unftp-sbe-fs](https://crates.io/crates/unftp-sbe-fs) - Stores files on the local filesystem * [unftp-sbe-gcs](https://crates.io/crates/unftp-sbe-gcs) - Stores files in Google Cloud Storage +* [unftp-sbe-rooter](https://crates.io/crates/unftp-sbe-rooter) - Wraps another storage back-end in order to root a user to a specific home directory. Known authentication back-ends: diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 8594afb..726518c 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,7 +1,6 @@ mod choose; mod restrict; -mod rooter; pub use choose::{ChoosingVfs, InnerVfs, SbeMeta}; pub use restrict::RestrictingVfs; -pub use rooter::{RooterVfs, UserWithRoot}; +pub use unftp_sbe_rooter::{RooterVfs, UserWithRoot}; diff --git a/src/storage/rooter.rs b/src/storage/rooter.rs deleted file mode 100644 index 628e83c..0000000 --- a/src/storage/rooter.rs +++ /dev/null @@ -1,342 +0,0 @@ -use async_trait::async_trait; -use libunftp::{ - auth::UserDetail, - storage::{Fileinfo, Metadata, Result, StorageBackend}, -}; -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; -use std::fmt::Debug; -use std::io::{Cursor, Error}; -use std::marker::PhantomData; -use std::path::{Component, Path, PathBuf}; -use tokio::io::AsyncRead; - -/// A virtual file system for libunftp that wraps other file systems -#[derive(Debug)] -pub struct RooterVfs -where - Delegate: StorageBackend, - User: UserWithRoot, - Meta: Metadata + Debug + Sync + Send, -{ - inner: Delegate, - x: PhantomData, - y: PhantomData, -} - -/// Used by [RooterVfs] to obtain the user's root path from a [UserDetail](libunftp::auth::UserDetail) implementation -pub trait UserWithRoot: UserDetail { - /// Returns the relative path to the user's root if it exists otherwise null. - fn user_root(&self) -> Option; -} - -impl RooterVfs -where - Delegate: StorageBackend, - User: UserWithRoot, - Meta: Metadata + Debug + Sync + Send, -{ - pub fn new(inner: Delegate) -> Self { - RooterVfs { - inner, - x: PhantomData, - y: PhantomData, - } - } - - pub(super) fn new_path<'a>(user: &User, requested_path: &'a Path) -> Cow<'a, Path> { - if let Some(user_root) = user.user_root() { - Cow::Owned(Self::root_to(user_root.as_os_str(), requested_path).unwrap()) - } else { - Cow::Borrowed(requested_path) - } - } - - fn root_to(root: &OsStr, requested_path: &Path) -> std::result::Result { - let mut iter = requested_path.components(); - - if let Some(first_component) = iter.next() { - let mut tokens = Vec::new(); - - match first_component { - Component::RootDir | Component::ParentDir => { - tokens.push(root); - } - Component::CurDir => { - return Err(()); // It should never start with . - } - _ => { - tokens.push(root); - tokens.push(first_component.as_os_str()); - } - } - - for component in iter { - match component { - Component::CurDir => {} - Component::ParentDir => { - let tokens_length = tokens.len(); - if tokens_length > 1 { - tokens.remove(tokens_length - 1); - } - } - _ => { - tokens.push(component.as_os_str()); - } - } - } - - let tokens_length = tokens.len(); - - let size = tokens - .iter() - .fold(tokens_length - 1, |acc, &x| acc + x.len()) - - 1; - - let mut path_string = OsString::with_capacity(size); - - for token in tokens.iter().take(tokens_length - 1) { - path_string.push(token); - path_string.push("/"); - } - - path_string.push(tokens[tokens_length - 1]); - - let path_buf = PathBuf::from(path_string); - - Ok(path_buf) - } else { - Err(()) // There will always be a prefix - } - } -} - -#[async_trait] -impl StorageBackend for RooterVfs -where - Delegate: StorageBackend, - User: UserWithRoot, - Meta: Metadata + Debug + Sync + Send, -{ - type Metadata = Delegate::Metadata; - - fn name(&self) -> &str { - self.inner.name() - } - - fn supported_features(&self) -> u32 { - self.inner.supported_features() - } - - async fn metadata + Send + Debug>( - &self, - user: &User, - path: P, - ) -> Result { - let path = Self::new_path(user, path.as_ref()); - self.inner.metadata(user, path).await - } - - async fn md5 + Send + Debug>(&self, user: &User, path: P) -> Result - where - P: AsRef + Send + Debug, - { - let path = Self::new_path(user, path.as_ref()); - self.inner.md5(user, path).await - } - - async fn list + Send + Debug>( - &self, - user: &User, - path: P, - ) -> Result>> - where - >::Metadata: Metadata, - { - let path = Self::new_path(user, path.as_ref()); - self.inner.list(user, path).await - } - - async fn list_fmt

(&self, user: &User, path: P) -> Result>> - where - P: AsRef + Send + Debug, - Self::Metadata: Metadata + 'static, - { - let path = Self::new_path(user, path.as_ref()); - self.inner.list_fmt(user, path).await - } - - async fn nlst

(&self, user: &User, path: P) -> std::result::Result>, Error> - where - P: AsRef + Send + Debug, - Self::Metadata: Metadata + 'static, - { - let path = Self::new_path(user, path.as_ref()); - self.inner.nlst(user, path).await - } - - async fn get_into<'a, P, W: ?Sized>( - &self, - user: &User, - path: P, - start_pos: u64, - output: &'a mut W, - ) -> Result - where - W: tokio::io::AsyncWrite + Unpin + Sync + Send, - P: AsRef + Send + Debug, - { - let path = Self::new_path(user, path.as_ref()); - self.inner.get_into(user, path, start_pos, output).await - } - - async fn get + Send + Debug>( - &self, - user: &User, - path: P, - start_pos: u64, - ) -> Result> { - let path = Self::new_path(user, path.as_ref()); - self.inner.get(user, path, start_pos).await - } - - async fn put< - P: AsRef + Send + Debug, - R: tokio::io::AsyncRead + Send + Sync + Unpin + 'static, - >( - &self, - user: &User, - input: R, - path: P, - start_pos: u64, - ) -> Result { - let path = Self::new_path(user, path.as_ref()); - self.inner.put(user, input, path, start_pos).await - } - - async fn del + Send + Debug>(&self, user: &User, path: P) -> Result<()> { - let path = Self::new_path(user, path.as_ref()); - self.inner.del(user, path).await - } - - async fn mkd + Send + Debug>(&self, user: &User, path: P) -> Result<()> { - let path = Self::new_path(user, path.as_ref()); - self.inner.mkd(user, path).await - } - - async fn rename + Send + Debug>( - &self, - user: &User, - from: P, - to: P, - ) -> Result<()> { - let from = Self::new_path(user, from.as_ref()); - let to = Self::new_path(user, to.as_ref()); - self.inner.rename(user, from, to).await - } - - async fn rmd + Send + Debug>(&self, user: &User, path: P) -> Result<()> { - let path = Self::new_path(user, path.as_ref()); - self.inner.rmd(user, path).await - } - - async fn cwd + Send + Debug>(&self, user: &User, path: P) -> Result<()> { - let path = Self::new_path(user, path.as_ref()); - self.inner.cwd(user, path).await - } -} - -#[cfg(test)] -mod tests { - use crate::auth::VfsOperations; - use pretty_assertions::assert_eq; - use std::path::{Path, PathBuf}; - - fn new_path(root: &str, requested: &str) -> PathBuf { - super::RooterVfs::::new_path( - &crate::auth::User { - username: "test".to_string(), - name: None, - surname: None, - account_enabled: false, - vfs_permissions: VfsOperations::all(), - allowed_mime_types: None, - root: Some(PathBuf::from(root)), - }, - Path::new(requested), - ) - .into() - } - - fn new_path_no_root(requested: &str) -> PathBuf { - super::RooterVfs::::new_path( - &crate::auth::User { - username: "test".to_string(), - name: None, - surname: None, - account_enabled: false, - vfs_permissions: VfsOperations::all(), - allowed_mime_types: None, - root: None, - }, - Path::new(requested), - ) - .into() - } - - #[test] - fn no_user_root_case() { - assert_eq!( - PathBuf::from("/my/documents/test.txt"), - new_path_no_root("/my/documents/test.txt") - ); - } - - #[test] - fn rooted_is_rerooted() { - assert_eq!( - PathBuf::from("alice/my/documents/test.txt"), - new_path("alice", "/my/documents/test.txt") - ); - } - - #[test] - fn relative_is_rooted() { - assert_eq!( - PathBuf::from("alice/my/documents/test.txt"), - new_path("alice", "my/documents/test.txt") - ); - } - - #[test] - fn cdups_is_ignored() { - assert_eq!( - PathBuf::from("alice/my/documents/test.txt"), - new_path("alice", "../../my/documents/test.txt") - ); - } - - #[test] - fn dots_removed_and_applied() { - assert_eq!( - PathBuf::from("alice/documents/test.txt"), - new_path("alice", "../../my/../.././documents/test.txt") - ); - } - - #[test] - fn user_root_with_trailing_slash() { - assert_eq!( - PathBuf::from("alice/documents/test.txt"), - new_path("alice/", "../../my/../.././documents/test.txt") - ); - } - - #[test] - fn user_root_with_slashed_in_front() { - assert_eq!( - PathBuf::from("/alice/documents/test.txt"), - new_path("/alice", "../../my/../.././documents/test.txt") - ); - } -}