diff --git a/README.md b/README.md index 2575367..ea86f59 100644 --- a/README.md +++ b/README.md @@ -207,20 +207,21 @@ curl http://192.168.8.10:5000/file --user user:pass --digest # digest aut Dufs supports account based access control. You can control who can do what on which path with `--auth`/`-a`. ``` -dufs -a user:pass@/path1:rw,/path2 -a user2:pass2@/path3 -a @/path4 +dufs -a admin:admin@/:rw -a guest:guest@/ +dufs -a user:pass@/:rw,/dir1,/dir2:- -a @/ ``` 1. Use `@` to separate the account and paths. No account means anonymous user. 2. Use `:` to separate the username and password of the account. 3. Use `,` to separate paths. -4. Use `:rw` suffix to indicate that the account has read-write permission on the path. +4. Use path suffix `:rw`, `:ro`, `:-` to set permissions: `read-write`, `read-only`, `forbidden`. `:ro` can be omitted. -- `-a admin:amdin@/:rw`: `admin` has complete permissions for all paths. +- `-a admin:admin@/:rw`: `admin` has complete permissions for all paths. - `-a guest:guest@/`: `guest` has read-only permissions for all paths. -- `-a user:pass@/dir1:rw,/dir2`: `user` has complete permissions for `/dir1/*`, has read-only permissions for `/dir2/`. +- `-a user:pass@/:rw,/dir1,/dir2:-`: `user` has read-write permissions for `/*`, has read-only permissions for `/dir1/*`, but is fordden for `/dir2/*`. - `-a @/`: All paths is publicly accessible, everyone can view/download it. -> There are no restrictions on using ':' and '@' characters in a password, `user:pa:ss@1@/:rw` is valid, and the password is `pa:ss@1`. +> There are no restrictions on using ':' and '@' characters in a password. For example, `user:pa:ss@1@/:rw` is valid, the password is `pa:ss@1`. #### Hashed Password diff --git a/src/auth.rs b/src/auth.rs index 78ba65a..6127cb7 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -49,46 +49,41 @@ impl AccessControl { } let new_raw_rules = split_rules(raw_rules); let mut use_hashed_password = false; - let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); - let mut anony = None; - let mut anony_paths = vec![]; - let mut users = IndexMap::new(); + let mut annoy_paths = None; + let mut account_paths_pairs = vec![]; for rule in &new_raw_rules { - let (account, paths) = split_account_paths(rule).ok_or_else(|| create_err(rule))?; - if account.is_empty() && anony.is_some() { - bail!("Invalid auth, duplicate anonymous rules"); - } - let mut access_paths = AccessPaths::default(); - for item in paths.trim_matches(',').split(',') { - let (path, perm) = match item.split_once(':') { - None => (item, AccessPerm::ReadOnly), - Some((path, "rw")) => (path, AccessPerm::ReadWrite), - _ => return Err(create_err(rule)), - }; - if account.is_empty() { - anony_paths.push((path, perm)); - } - access_paths.add(path, perm); - } + let (account, paths) = + split_account_paths(rule).ok_or_else(|| anyhow!("Invalid auth `{rule}`"))?; if account.is_empty() { - anony = Some(access_paths); + if annoy_paths.is_some() { + bail!("Invalid auth, no duplicate anonymous rules"); + } + annoy_paths = Some(paths) } else if let Some((user, pass)) = account.split_once(':') { if user.is_empty() || pass.is_empty() { - return Err(create_err(rule)); + bail!("Invalid auth `{rule}`"); } - if pass.starts_with("$6$") { - use_hashed_password = true; - } - users.insert(user.to_string(), (pass.to_string(), access_paths)); - } else { - return Err(create_err(rule)); + account_paths_pairs.push((user, pass, paths)); } } - for (path, perm) in anony_paths { - for (_, (_, paths)) in users.iter_mut() { - paths.add(path, perm) - } + let mut anony = None; + if let Some(paths) = annoy_paths { + let mut access_paths = AccessPaths::default(); + access_paths.merge(paths); + anony = Some(access_paths); } + let mut users = IndexMap::new(); + for (user, pass, paths) in account_paths_pairs.into_iter() { + let mut access_paths = anony.clone().unwrap_or_default(); + access_paths + .merge(paths) + .ok_or_else(|| anyhow!("Invalid auth `{user}:{pass}@{paths}"))?; + if pass.starts_with("$6$") { + use_hashed_password = true; + } + users.insert(user.to_string(), (pass.to_string(), access_paths)); + } + Ok(Self { use_hashed_password, users, @@ -151,13 +146,27 @@ impl AccessPaths { self.perm } - fn set_perm(&mut self, perm: AccessPerm) { - if self.perm < perm { - self.perm = perm + pub fn set_perm(&mut self, perm: AccessPerm) { + if !perm.inherit() { + self.perm = perm; } } - pub fn add(&mut self, path: &str, perm: AccessPerm) { + pub fn merge(&mut self, paths: &str) -> Option<()> { + for item in paths.trim_matches(',').split(',') { + let (path, perm) = match item.split_once(':') { + None => (item, AccessPerm::ReadOnly), + Some((path, "ro")) => (path, AccessPerm::ReadOnly), + Some((path, "rw")) => (path, AccessPerm::ReadWrite), + Some((path, "-")) => (path, AccessPerm::Forbidden), + _ => return None, + }; + self.add(path, perm); + } + Some(()) + } + + fn add(&mut self, path: &str, perm: AccessPerm) { let path = path.trim_matches('/'); if path.is_empty() { self.set_perm(perm); @@ -184,6 +193,9 @@ impl AccessPaths { .filter(|v| !v.is_empty()) .collect(); let target = self.find_impl(&parts, self.perm)?; + if target.perm().forbidden() { + return None; + } if writable && !target.perm().readwrite() { return None; } @@ -191,13 +203,13 @@ impl AccessPaths { } fn find_impl(&self, parts: &[&str], perm: AccessPerm) -> Option { - let perm = if !self.perm.indexonly() { + let perm = if !self.perm.inherit() { self.perm } else { perm }; if parts.is_empty() { - if perm.indexonly() { + if perm.inherit() { return Some(self.clone()); } else { return Some(AccessPaths::new(perm)); @@ -206,7 +218,7 @@ impl AccessPaths { let child = match self.children.get(parts[0]) { Some(v) => v, None => { - if perm.indexonly() { + if perm.inherit() { return None; } else { return Some(AccessPaths::new(perm)); @@ -221,7 +233,7 @@ impl AccessPaths { } pub fn child_paths(&self, base: &Path) -> Vec { - if !self.perm().indexonly() { + if !self.perm().inherit() { return vec![base.to_path_buf()]; } let mut output = vec![]; @@ -232,7 +244,7 @@ impl AccessPaths { fn child_paths_impl(&self, output: &mut Vec, base: &Path) { for (name, child) in self.children.iter() { let base = base.join(name); - if child.perm().indexonly() { + if child.perm().inherit() { child.child_paths_impl(output, &base); } else { output.push(base) @@ -244,18 +256,23 @@ impl AccessPaths { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum AccessPerm { #[default] - IndexOnly, + Inherit, ReadOnly, ReadWrite, + Forbidden, } impl AccessPerm { + pub fn inherit(&self) -> bool { + self == &AccessPerm::Inherit + } + pub fn readwrite(&self) -> bool { self == &AccessPerm::ReadWrite } - pub fn indexonly(&self) -> bool { - self == &AccessPerm::IndexOnly + pub fn forbidden(&self) -> bool { + self == &AccessPerm::Forbidden } } @@ -559,6 +576,7 @@ mod tests { paths.add("/dir1", AccessPerm::ReadWrite); paths.add("/dir2/dir21", AccessPerm::ReadWrite); paths.add("/dir2/dir21/dir211", AccessPerm::ReadOnly); + paths.add("/dir2/dir21/dir212", AccessPerm::Forbidden); paths.add("/dir2/dir22", AccessPerm::ReadOnly); paths.add("/dir2/dir22/dir221", AccessPerm::ReadWrite); paths.add("/dir2/dir23/dir231", AccessPerm::ReadWrite); @@ -603,17 +621,6 @@ mod tests { Some(AccessPaths::new(AccessPerm::ReadOnly)) ); assert_eq!(paths.find("dir2/dir21/dir211/file", true), None); - } - - #[test] - fn test_access_paths_perm() { - let mut paths = AccessPaths::default(); - assert_eq!(paths.perm(), AccessPerm::IndexOnly); - paths.set_perm(AccessPerm::ReadOnly); - assert_eq!(paths.perm(), AccessPerm::ReadOnly); - paths.set_perm(AccessPerm::ReadWrite); - assert_eq!(paths.perm(), AccessPerm::ReadWrite); - paths.set_perm(AccessPerm::ReadOnly); - assert_eq!(paths.perm(), AccessPerm::ReadWrite); + assert_eq!(paths.find("dir2/dir21/dir212", false), None); } } diff --git a/src/server.rs b/src/server.rs index 6324317..a905e05 100644 --- a/src/server.rs +++ b/src/server.rs @@ -345,7 +345,7 @@ impl Server { method => match method.as_str() { "PROPFIND" => { if is_dir { - let access_paths = if access_paths.perm().indexonly() { + let access_paths = if access_paths.perm().inherit() { // see https://github.com/sigoden/dufs/issues/229 AccessPaths::new(AccessPerm::ReadOnly) } else { @@ -1183,7 +1183,7 @@ impl Server { access_paths: AccessPaths, ) -> Result> { let mut paths: Vec = vec![]; - if access_paths.perm().indexonly() { + if access_paths.perm().inherit() { for name in access_paths.child_names() { let entry_path = entry_path.join(name); self.add_pathitem(&mut paths, base_path, &entry_path).await; diff --git a/tests/auth.rs b/tests/auth.rs index e290916..f22af6d 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -144,6 +144,23 @@ fn auth_readonly( Ok(()) } +#[rstest] +fn auth_forbidden( + #[with(&["--auth", "user:pass@/:rw,/dir1:-", "-A"])] server: TestServer, +) -> Result<(), Error> { + let url = format!("{}file1", server.url()); + let resp = fetch!(b"PUT", &url) + .body(b"abc".to_vec()) + .send_with_digest_auth("user", "pass")?; + assert_eq!(resp.status(), 201); + let url = format!("{}dir1/file1", server.url()); + let resp = fetch!(b"PUT", &url) + .body(b"abc".to_vec()) + .send_with_digest_auth("user", "pass")?; + assert_eq!(resp.status(), 403); + Ok(()) +} + #[rstest] fn auth_nest( #[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "--auth", "user3:pass3@/dir1:rw", "-A"])]