Skip to content

Commit

Permalink
Improve libzauth ACL syntax (#2714)
Browse files Browse the repository at this point in the history
This PR replaces the prefix-tree matcher used in libzauth for matching ACL
paths with a simple regex-based matcher, which constructs a single regular
expression containing all possible paths. This makes it trivial to accept
user-provided regular expressions in the ACL language itself.
  • Loading branch information
pcapriotti authored Sep 23, 2022
1 parent d23f767 commit 74632d7
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 153 deletions.
1 change: 1 addition & 0 deletions changelog.d/5-internal/improve-acl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add regular expression support to libzauth ACL language
42 changes: 41 additions & 1 deletion libs/libzauth/libzauth-c/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion libs/libzauth/libzauth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zauth"
version = "3.0.0"
version = "3.1.0"
authors = ["Wire Swiss GmbH <[email protected]>"]
license = "AGPL-3.0"

Expand All @@ -11,6 +11,8 @@ name = "zauth"
asexp = ">= 0.3"
rustc-serialize = ">= 0.3"
sodiumoxide = "^0.2.7"
regex = "1.6"
lazy_static = "1.4"

[dev-dependencies]
clap = ">= 2.0"
99 changes: 47 additions & 52 deletions libs/libzauth/libzauth/src/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,34 @@
// You should have received a copy of the GNU Affero General Public License along
// with this program. If not, see <https://www.gnu.org/licenses/>.

use std::collections::HashMap;
use asexp::Sexp;
use tree::Tree;
use matcher::{Item, Matcher};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub enum Error {
Parse(&'static str)
Parse(&'static str),
}

pub type AclResult<A> = Result<A, Error>;

#[derive(Debug, Clone)]
pub struct Acl {
acl: HashMap<String, List>
acl: HashMap<String, List>,
}

impl Acl {
pub fn new() -> Acl {
Acl { acl: HashMap::new() }
Acl {
acl: HashMap::new(),
}
}

pub fn from_str(s: &str) -> AclResult<Acl> {
match Sexp::parse_toplevel(s) {
Err(()) => Err(Error::Parse("invalid s-expressions")),
Ok(sexp) => Acl::from_sexp(&sexp)
let sexp = Sexp::parse_toplevel(s);
match sexp {
Err(()) => Err(Error::Parse("invalid s-expressions")),
Ok(sexp) => Acl::from_sexp(&sexp),
}
}

Expand All @@ -51,82 +54,69 @@ impl Acl {
if let Some(k) = key.get_str().map(String::from) {
acl.insert(k, List::from_sexp(&list)?);
} else {
return Err(Error::Parse("not a string"))
return Err(Error::Parse("not a string"));
}
}
Ok(Acl { acl })
}
_ => Err(Error::Parse("expected key and values"))
_ => Err(Error::Parse("expected key and values")),
}
}

pub fn allowed(&self, key: &str, path: &str) -> bool {
self.acl.get(key).map(|list| {
match *list {
List::Black(Some(ref t)) => !t.contains(path),
List::Black(None) => true,
List::White(Some(ref t)) => t.contains(path),
List::White(None) => false
}
}).unwrap_or(false)
self.acl
.get(key)
.map(|list| match *list {
List::Black(ref t) => !t.contains(path),
List::White(ref t) => t.contains(path),
})
.unwrap_or(false)
}
}

#[derive(Debug, Clone)]
enum List {
Black(Option<Tree>),
White(Option<Tree>)
Black(Matcher),
White(Matcher),
}

impl List {
fn from_sexp(s: &Sexp) -> AclResult<List> {
let items = match *s {
Sexp::Tuple(ref a) => a.as_slice(),
Sexp::Array(ref a) => a.as_slice(),
_ => return Err(Error::Parse("s-expr not a list"))
_ => return Err(Error::Parse("s-expr not a list")),
};

if items.is_empty() {
return Err(Error::Parse("list is empty"))
return Err(Error::Parse("list is empty"));
}

match items[0].get_str() {
Some("blacklist") => List::items(&items[1 ..]).map(List::Black),
Some("whitelist") => List::items(&items[1 ..]).map(List::White),
_ => Err(Error::Parse("'blacklist' or 'whitelist' expected"))
Some("blacklist") => List::items(&items[1..]).map(List::Black),
Some("whitelist") => List::items(&items[1..]).map(List::White),
_ => Err(Error::Parse("'blacklist' or 'whitelist' expected")),
}
}

fn items(xs: &[Sexp]) -> AclResult<Option<Tree>> {
match xs.len() {
0 => Ok(None),
1 if List::is_unit(&xs[0]) => Ok(None),
_ => {
let mut t = Tree::new();
for x in xs {
t.add(&List::read_path(x)?)
}
Ok(Some(t))
}
}
fn items(xs: &[Sexp]) -> AclResult<Matcher> {
let items: AclResult<Vec<_>> = xs.iter().map(List::read_path).collect();
let m = Matcher::new(&items?);
Ok(m)
}

fn is_unit(s: &Sexp) -> bool {
match *s {
Sexp::Tuple(ref a) if a.is_empty() => true,
_ => false
}
}

fn read_path(s: &Sexp) -> AclResult<String> {
fn read_path(s: &Sexp) -> AclResult<Item> {
match *s {
Sexp::Tuple(ref a) | Sexp::Array(ref a) if a.len() == 2 => {
match (a[0].get_str(), a[1].get_str()) {
(Some("path"), Some(x)) => Ok(String::from(x)),
_ => Err(Error::Parse("'path' not found"))
(Some("path"), Some(x)) => Ok(Item::Str(String::from(x))),
(Some("regex"), Some(x)) => {
Ok(Item::Regex(String::from(x)))
}
_ => Err(Error::Parse("'path' not found")),
}
}
_ => return Err(Error::Parse("s-expr not a list"))
_ => return Err(Error::Parse("s-expr not a list")),
}
}
}
Expand All @@ -144,14 +134,15 @@ mod tests {
(path "/a/**"))
b (whitelist (path "/conversation/message")
(path "/foo/bar/*"))
(path "/foo/bar/*")
(regex "(/v[0-9]+)?/foo/baz/[^/]+"))
# this is a comment that should not lead to a parse failure.
la (whitelist (path "/legalhold/**"))
x (blacklist ())
x (blacklist)
y (whitelist ())
y (whitelist)
"#;

#[test]
Expand All @@ -165,8 +156,12 @@ mod tests {
assert!(!acl.allowed("u", "/x/here/z"));
assert!(acl.allowed("u", "/x/here/z/x"));
assert!(acl.allowed("b", "/conversation/message"));
assert!(acl.allowed("b", "/foo/bar/baz"));
assert!(acl.allowed("b", "/foo/bar/quux"));
assert!(!acl.allowed("b", "/foo/bar/"));
assert!(acl.allowed("b", "/foo/baz/quux"));
assert!(!acl.allowed("b", "/foo/bar/"));
assert!(acl.allowed("b", "/v97/foo/baz/quux"));
assert!(!acl.allowed("b", "/voo/foo/baz/quux"));
assert!(!acl.allowed("b", "/anywhere/else/"));
assert!(acl.allowed("x", "/everywhere"));
assert!(acl.allowed("x", "/"));
Expand Down
4 changes: 3 additions & 1 deletion libs/libzauth/libzauth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.

extern crate asexp;
extern crate lazy_static;
extern crate regex;
extern crate rustc_serialize;
extern crate sodiumoxide;

pub mod acl;
pub mod error;
pub mod zauth;

mod tree;
mod matcher;

pub use acl::Acl;
pub use error::Error;
Expand Down
Loading

0 comments on commit 74632d7

Please sign in to comment.