use std::collections::BTreeSet;
pub fn permission_matches(stored: &str, wanted: &str) -> bool {
if stored == "*" || stored == wanted {
return true;
}
let Some(prefix) = stored.strip_suffix(".*") else { return false };
wanted.len() > prefix.len()
&& wanted.starts_with(prefix)
&& wanted.as_bytes()[prefix.len()] == b'.'
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Grants {
pub roles: BTreeSet<String>,
pub granted: BTreeSet<String>,
pub denied: BTreeSet<String>,
pub is_super: bool,
}
impl Grants {
pub fn denies(&self, permission: &str) -> bool {
self.denied.iter().any(|stored| permission_matches(stored, permission))
}
pub fn allows(&self, permission: &str) -> bool {
if self.denies(permission) {
return false;
}
if self.is_super {
return true;
}
self.granted.iter().any(|stored| permission_matches(stored, permission))
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.contains(role)
}
pub fn effective(&self) -> Vec<String> {
self.granted
.iter()
.filter(|stored| !self.denies(stored))
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set<const N: usize>(names: [&str; N]) -> BTreeSet<String> {
names.iter().map(|n| (*n).to_string()).collect()
}
#[test]
fn an_exact_name_matches_itself_and_nothing_near_it() {
assert!(permission_matches("users.create", "users.create"));
assert!(!permission_matches("users.create", "users.createx"));
assert!(!permission_matches("users.create", "users.delete"));
assert!(!permission_matches("users.create", "users"));
assert!(!permission_matches("users.create", ""));
}
#[test]
fn a_bare_star_matches_anything() {
for wanted in ["users.create", "a", "", "*", "a.b.c.d"] {
assert!(permission_matches("*", wanted), "`*` should cover {wanted:?}");
}
}
#[test]
fn a_trailing_star_matches_every_segment_below_it() {
assert!(permission_matches("users.*", "users.create"));
assert!(permission_matches("users.*", "users.avatar.delete"));
assert!(!permission_matches("users.*", "users"));
assert!(!permission_matches("users.*", "usersomething"));
assert!(!permission_matches("users.*", "user.create"));
assert!(!permission_matches("users.*", "posts.create"));
}
#[test]
fn a_star_anywhere_else_is_just_a_character() {
assert!(!permission_matches("users.*.create", "users.avatar.create"));
assert!(permission_matches("users.*.create", "users.*.create"));
assert!(!permission_matches("user*", "users"));
}
#[test]
fn the_wildcard_only_works_on_the_stored_side() {
assert!(!permission_matches("users.create", "users.*"));
assert!(!permission_matches("users.create", "*"));
}
#[test]
fn a_grant_allows_and_silence_refuses() {
let grants = Grants { granted: set(["users.create"]), ..Grants::default() };
assert!(grants.allows("users.create"));
assert!(!grants.allows("users.delete"));
assert!(!Grants::default().allows("users.create"));
}
#[test]
fn a_direct_deny_beats_a_grant() {
let grants = Grants {
granted: set(["users.create", "users.delete"]),
denied: set(["users.delete"]),
..Grants::default()
};
assert!(grants.allows("users.create"));
assert!(!grants.allows("users.delete"), "the deny has to win");
}
#[test]
fn a_direct_deny_beats_a_wildcard_grant() {
let grants = Grants {
granted: set(["users.*"]),
denied: set(["users.delete"]),
..Grants::default()
};
assert!(grants.allows("users.create"));
assert!(!grants.allows("users.delete"));
}
#[test]
fn a_wildcard_deny_shuts_a_whole_prefix() {
let grants =
Grants { granted: set(["*"]), denied: set(["billing.*"]), ..Grants::default() };
assert!(grants.allows("users.create"));
assert!(!grants.allows("billing.refund"));
assert!(grants.allows("billing"));
}
#[test]
fn a_deny_beats_even_a_super_role() {
let grants =
Grants { is_super: true, denied: set(["billing.refund"]), ..Grants::default() };
assert!(grants.allows("anything.at.all"));
assert!(
!grants.allows("billing.refund"),
"an explicit deny is the one thing a super role does not overrule"
);
}
#[test]
fn a_super_role_needs_no_permissions_attached() {
let grants = Grants { is_super: true, ..Grants::default() };
assert!(grants.allows("users.create"));
assert!(grants.allows("something.nobody.has.defined.yet"));
assert!(grants.granted.is_empty(), "which is exactly why it cannot be audited");
}
#[test]
fn roles_are_matched_by_exact_name() {
let grants = Grants { roles: set(["admin", "billing.manager"]), ..Grants::default() };
assert!(grants.has_role("admin"));
assert!(grants.has_role("billing.manager"));
assert!(!grants.has_role("Admin"));
assert!(!grants.has_role("billing.*"), "role names are not patterns");
assert!(!grants.has_role("editor"));
}
#[test]
fn effective_lists_stored_names_with_the_denied_ones_removed() {
let grants = Grants {
granted: set(["users.create", "users.delete", "posts.*"]),
denied: set(["users.delete", "posts.publish"]),
..Grants::default()
};
assert_eq!(grants.effective(), vec!["posts.*", "users.create"]);
}
#[test]
fn precedence_holds_in_every_combination() {
for &deny in &[false, true] {
for &is_super in &[false, true] {
for &grant in &[false, true] {
let grants = Grants {
granted: if grant { set(["users.create"]) } else { BTreeSet::new() },
denied: if deny { set(["users.create"]) } else { BTreeSet::new() },
is_super,
..Grants::default()
};
let expected = !deny && (is_super || grant);
assert_eq!(
grants.allows("users.create"),
expected,
"deny={deny} super={is_super} grant={grant}"
);
}
}
}
}
}