feat: auth supports forbidden permissions (#329)
This commit is contained in:
parent
006e03ed30
commit
af347f9cf0
4 changed files with 88 additions and 63 deletions
11
README.md
11
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
|
||||
|
||||
|
|
119
src/auth.rs
119
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<AccessPaths> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf>, 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vec<PathItem>> {
|
||||
let mut paths: Vec<PathItem> = 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;
|
||||
|
|
|
@ -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"])]
|
||||
|
|
Loading…
Reference in a new issue