use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
pub struct PermissionSet {
superuser: bool,
grants: HashSet<String>,
allow: HashSet<String>,
deny: HashSet<String>,
}
impl PermissionSet {
pub fn new(superuser: bool, grants: impl IntoIterator<Item = String>) -> Self {
Self {
superuser,
grants: grants.into_iter().collect(),
allow: HashSet::new(),
deny: HashSet::new(),
}
}
pub fn with_overrides(
superuser: bool,
grants: impl IntoIterator<Item = String>,
allow: impl IntoIterator<Item = String>,
deny: impl IntoIterator<Item = String>,
) -> Self {
Self {
superuser,
grants: grants.into_iter().collect(),
allow: allow.into_iter().collect(),
deny: deny.into_iter().collect(),
}
}
pub fn is_superuser(&self) -> bool {
self.superuser
}
pub fn allows(&self, needed: &str) -> bool {
if self.superuser {
return true;
}
if self.deny.contains(needed) {
return false;
}
if self.allow.contains(needed) {
return true;
}
self.role_allows(needed)
}
fn role_allows(&self, needed: &str) -> bool {
if self.grants.contains("*") || self.grants.contains(needed) {
return true;
}
needed.match_indices('.').any(|(dot, _)| {
let mut wildcard = String::with_capacity(dot + 2);
wildcard.push_str(&needed[..=dot]);
wildcard.push('*');
self.grants.contains(&wildcard)
})
}
pub fn grants(&self) -> impl Iterator<Item = &str> {
self.grants.iter().map(String::as_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set(grants: &[&str]) -> PermissionSet {
PermissionSet::new(false, grants.iter().map(|s| s.to_string()))
}
#[test]
fn exact_grant_matches() {
let p = set(&["posts.approve"]);
assert!(p.allows("posts.approve"));
assert!(!p.allows("posts.edit"));
}
#[test]
fn namespace_wildcard_matches_descendants_only() {
let p = set(&["posts.*"]);
assert!(p.allows("posts.approve"));
assert!(p.allows("posts.edit"));
assert!(p.allows("posts.tags.create"));
assert!(!p.allows("posts"));
assert!(!p.allows("users.edit"));
}
#[test]
fn global_wildcard_and_superuser_match_everything() {
assert!(set(&["*"]).allows("anything.at.all"));
let su = PermissionSet::new(true, std::iter::empty());
assert!(su.allows("anything.at.all"));
assert!(su.is_superuser());
}
#[test]
fn empty_set_grants_nothing() {
assert!(!set(&[]).allows("posts.approve"));
}
#[test]
fn user_deny_overrides_a_role_grant() {
let p = PermissionSet::with_overrides(
false,
["posts.*".to_string()],
std::iter::empty(),
["posts.approve".to_string()],
);
assert!(p.allows("posts.edit"));
assert!(!p.allows("posts.approve"));
}
#[test]
fn user_allow_grants_beyond_the_role() {
let p = PermissionSet::with_overrides(
false,
std::iter::empty(),
["posts.approve".to_string()],
std::iter::empty(),
);
assert!(p.allows("posts.approve"));
assert!(!p.allows("posts.edit"));
}
#[test]
fn deny_wins_over_allow_and_superuser_ignores_overrides() {
let denied = PermissionSet::with_overrides(
false,
std::iter::empty(),
["x.y".to_string()],
["x.y".to_string()],
);
assert!(!denied.allows("x.y"));
let su = PermissionSet::with_overrides(
true,
std::iter::empty(),
std::iter::empty(),
["x.y".to_string()],
);
assert!(su.allows("x.y"));
}
}