unftp_sbe_restrict/
lib.rs

1#![deny(clippy::all)]
2#![deny(missing_docs)]
3#![forbid(unsafe_code)]
4#![doc(html_root_url = "https://docs.rs/unftp-sbe-restrict/0.1.1")]
5
6//! A [libunftp](https://docs.rs/libunftp/latest/libunftp/) wrapper
7//! storage back-end that restricts FTP operations and in so doing
8//! provide some form of authorization.
9//!
10//! # Quick start
11//!
12//! Start by implementing the libunftp [`UserDetail`](libunftp::auth::UserDetail) trait
13//! and then follow that by implementing [`UserWithPermissions`](crate::UserWithPermissions).
14//!
15//! Finally call the [RestrictingVfs::new()](crate::RestrictingVfs::new) method.
16//!
17//! ```rust
18//! use libunftp::auth::UserDetail;
19//! use unftp_sbe_restrict::{UserWithPermissions, VfsOperations};
20//! use std::fmt::Formatter;
21//!
22//! #[derive(Debug, PartialEq, Eq)]
23//! pub struct User {
24//!     pub username: String,
25//!     // e.g. this can be something like
26//!     // `VfsOperations::all() - VfsOperations::PUT - VfsOperations::DEL`
27//!     pub permissions: VfsOperations,
28//! }
29//!
30//! impl UserDetail for User {
31//!     fn account_enabled(&self) -> bool {
32//!         true
33//!     }
34//! }
35//!
36//! impl std::fmt::Display for User {
37//!     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38//!         write!(f, "User(username: {:?}", self.username,)
39//!     }
40//! }
41//!
42//! impl UserWithPermissions for User {
43//!     fn permissions(&self) -> VfsOperations {
44//!         self.permissions
45//!     }
46//! }
47//!
48//! // Return type omited for brevity.
49//! fn create_restricted_storage_backend() {
50//!     use unftp_sbe_fs::{Filesystem, Meta};
51//!     let _backend = Box::new(move || {
52//!         unftp_sbe_restrict::RestrictingVfs::<Filesystem, User, Meta>::new(Filesystem::new("/srv/ftp"))
53//!     });
54//! }
55//!
56// ```
57
58use async_trait::async_trait;
59use bitflags::bitflags;
60use libunftp::{
61    auth::UserDetail,
62    storage::{self, Fileinfo, Metadata, StorageBackend},
63};
64use std::fmt::Debug;
65use std::io::{Cursor, Error, ErrorKind};
66use std::marker::PhantomData;
67use std::path::{Path, PathBuf};
68use tokio::io::AsyncRead;
69
70bitflags! {
71    /// The FTP operations that can be enabled/disabled for the virtual filesystem.
72    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73    pub struct VfsOperations: u32 {
74        /// If set allows FTP make directory
75        const MK_DIR = 0b00000001;
76        /// If set allows FTP remove directory
77        const RM_DIR = 0b00000010;
78        /// If set allows FTP GET i.e. clients can download files.
79        const GET    = 0b00000100;
80        /// If set allows FTP PUT i.e. clients can upload files.
81        const PUT    = 0b00001000;
82        /// If set allows FTP DELE i.e. clients can remove files.
83        const DEL    = 0b00010000;
84        /// If set allows FTP RENAME i.e. clients can rename directories and files
85        const RENAME = 0b00100000;
86        /// If set allows the extended SITE MD5 command to calculate checksums
87        const MD5    = 0b01000000;
88        /// If set allows clients to list the contents of a directory.
89        const LIST   = 0b10000000;
90
91        /// Convenience aggragation of all the write operation bits.
92        const WRITE_OPS = Self::MK_DIR.bits() | Self::RM_DIR.bits() | Self::PUT.bits() | Self::DEL.bits() | Self::RENAME.bits();
93    }
94}
95
96/// Used by [RestrictingVfs] to obtain permission info from a [UserDetail](libunftp::auth::UserDetail) implementation
97pub trait UserWithPermissions: UserDetail {
98    /// Returns the permissions given to the user
99    fn permissions(&self) -> VfsOperations;
100}
101
102/// A virtual filesystem that checks if the user has permissions to do its operations before it
103/// delegates to another storage back-end.
104#[derive(Debug)]
105pub struct RestrictingVfs<Delegate, User, Meta>
106where
107    Delegate: StorageBackend<User>,
108    User: UserWithPermissions,
109    Meta: Metadata + Debug + Sync + Send,
110{
111    delegate: Delegate,
112    x: PhantomData<Meta>,
113    y: PhantomData<User>,
114}
115
116impl<Delegate, User, Meta> RestrictingVfs<Delegate, User, Meta>
117where
118    Delegate: StorageBackend<User>,
119    User: UserWithPermissions,
120    Meta: Metadata + Debug + Sync + Send,
121{
122    /// Creates a new instance of [`RestrictingVfs`](crate::RestrictingVfs).
123    pub fn new(delegate: Delegate) -> Self {
124        RestrictingVfs {
125            delegate,
126            x: PhantomData,
127            y: PhantomData,
128        }
129    }
130}
131
132#[async_trait]
133impl<Delegate, User, Meta> StorageBackend<User> for RestrictingVfs<Delegate, User, Meta>
134where
135    Delegate: StorageBackend<User>,
136    User: UserWithPermissions,
137    Meta: Metadata + Debug + Sync + Send,
138{
139    type Metadata = Delegate::Metadata;
140
141    fn name(&self) -> &str {
142        self.delegate.name()
143    }
144
145    fn supported_features(&self) -> u32 {
146        self.delegate.supported_features()
147    }
148
149    async fn metadata<P: AsRef<Path> + Send + Debug>(
150        &self,
151        user: &User,
152        path: P,
153    ) -> storage::Result<Self::Metadata> {
154        self.delegate.metadata(user, path).await
155    }
156
157    async fn md5<P: AsRef<Path> + Send + Debug>(
158        &self,
159        user: &User,
160        path: P,
161    ) -> storage::Result<String>
162    where
163        P: AsRef<Path> + Send + Debug,
164    {
165        if user.permissions().contains(VfsOperations::MD5) {
166            self.delegate.md5(user, path).await
167        } else {
168            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
169        }
170    }
171
172    async fn list<P: AsRef<Path> + Send + Debug>(
173        &self,
174        user: &User,
175        path: P,
176    ) -> storage::Result<Vec<Fileinfo<PathBuf, Self::Metadata>>>
177    where
178        <Self as StorageBackend<User>>::Metadata: Metadata,
179    {
180        if user.permissions().contains(VfsOperations::LIST) {
181            self.delegate.list(user, path).await
182        } else {
183            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
184        }
185    }
186
187    async fn list_fmt<P>(&self, user: &User, path: P) -> storage::Result<Cursor<Vec<u8>>>
188    where
189        P: AsRef<Path> + Send + Debug,
190        Self::Metadata: Metadata + 'static,
191    {
192        if user.permissions().contains(VfsOperations::LIST) {
193            self.delegate.list_fmt(user, path).await
194        } else {
195            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
196        }
197    }
198
199    async fn nlst<P>(&self, user: &User, path: P) -> std::result::Result<Cursor<Vec<u8>>, Error>
200    where
201        P: AsRef<Path> + Send + Debug,
202        Self::Metadata: Metadata + 'static,
203    {
204        if user.permissions().contains(VfsOperations::LIST) {
205            self.delegate.nlst(user, path).await
206        } else {
207            Err(ErrorKind::PermissionDenied.into())
208        }
209    }
210
211    async fn get_into<'a, P, W: ?Sized>(
212        &self,
213        user: &User,
214        path: P,
215        start_pos: u64,
216        output: &'a mut W,
217    ) -> storage::Result<u64>
218    where
219        W: tokio::io::AsyncWrite + Unpin + Sync + Send,
220        P: AsRef<Path> + Send + Debug,
221    {
222        if user.permissions().contains(VfsOperations::GET) {
223            self.delegate.get_into(user, path, start_pos, output).await
224        } else {
225            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
226        }
227    }
228
229    async fn get<P: AsRef<Path> + Send + Debug>(
230        &self,
231        user: &User,
232        path: P,
233        start_pos: u64,
234    ) -> storage::Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
235        if user.permissions().contains(VfsOperations::GET) {
236            self.delegate.get(user, path, start_pos).await
237        } else {
238            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
239        }
240    }
241
242    async fn put<
243        P: AsRef<Path> + Send + Debug,
244        R: tokio::io::AsyncRead + Send + Sync + Unpin + 'static,
245    >(
246        &self,
247        user: &User,
248        input: R,
249        path: P,
250        start_pos: u64,
251    ) -> storage::Result<u64> {
252        if user.permissions().contains(VfsOperations::PUT) {
253            self.delegate.put(user, input, path, start_pos).await
254        } else {
255            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
256        }
257    }
258
259    async fn del<P: AsRef<Path> + Send + Debug>(
260        &self,
261        user: &User,
262        path: P,
263    ) -> storage::Result<()> {
264        if user.permissions().contains(VfsOperations::DEL) {
265            self.delegate.del(user, path).await
266        } else {
267            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
268        }
269    }
270
271    async fn mkd<P: AsRef<Path> + Send + Debug>(
272        &self,
273        user: &User,
274        path: P,
275    ) -> storage::Result<()> {
276        if user.permissions().contains(VfsOperations::MK_DIR) {
277            self.delegate.mkd(user, path).await
278        } else {
279            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
280        }
281    }
282
283    async fn rename<P: AsRef<Path> + Send + Debug>(
284        &self,
285        user: &User,
286        from: P,
287        to: P,
288    ) -> storage::Result<()> {
289        if user.permissions().contains(VfsOperations::RENAME) {
290            self.delegate.rename(user, from, to).await
291        } else {
292            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
293        }
294    }
295
296    async fn rmd<P: AsRef<Path> + Send + Debug>(
297        &self,
298        user: &User,
299        path: P,
300    ) -> storage::Result<()> {
301        if user.permissions().contains(VfsOperations::RM_DIR) {
302            self.delegate.rmd(user, path).await
303        } else {
304            Err(libunftp::storage::ErrorKind::PermissionDenied.into())
305        }
306    }
307
308    async fn cwd<P: AsRef<Path> + Send + Debug>(
309        &self,
310        user: &User,
311        path: P,
312    ) -> storage::Result<()> {
313        self.delegate.cwd(user, path).await
314    }
315}