use lru::LruCache;
use std::num::NonZeroUsize;
use crate::aci::{Aci, AuthorizationResult};
use crate::entry::LdapEntry;
use crate::operation::LdapOperation;
use crate::optimized::LdapCompiledEvaluator;
type CacheKey = u64;
struct PreHashedPassthrough(u64);
impl std::hash::Hasher for PreHashedPassthrough {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = self.0.wrapping_mul(31).wrapping_add(b as u64);
}
}
fn write_u64(&mut self, n: u64) {
self.0 = n;
}
}
#[derive(Clone, Default)]
struct PassthroughBuildHasher;
impl std::hash::BuildHasher for PassthroughBuildHasher {
type Hasher = PreHashedPassthrough;
fn build_hasher(&self) -> Self::Hasher {
PreHashedPassthrough(0)
}
}
pub const DEFAULT_CACHE_SIZE: usize = 1024;
pub struct CachedAciPolicy {
evaluator: LdapCompiledEvaluator,
check_cache: LruCache<CacheKey, bool, PassthroughBuildHasher>,
authorize_cache: LruCache<CacheKey, bool, PassthroughBuildHasher>,
}
impl std::fmt::Debug for CachedAciPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CachedAciPolicy")
.field("evaluator", &self.evaluator)
.field("check_cache_len", &self.check_cache.len())
.field("authorize_cache_len", &self.authorize_cache.len())
.finish()
}
}
impl CachedAciPolicy {
pub fn new(acis: &[Aci]) -> Self {
Self::with_cache_size(acis, DEFAULT_CACHE_SIZE)
}
pub fn with_cache_size(acis: &[Aci], cache_size: usize) -> Self {
let size = NonZeroUsize::new(cache_size.max(1)).unwrap();
Self {
evaluator: LdapCompiledEvaluator::build(acis),
check_cache: LruCache::with_hasher(size, PassthroughBuildHasher),
authorize_cache: LruCache::with_hasher(size, PassthroughBuildHasher),
}
}
pub fn check_access(
&mut self,
operation: &LdapOperation,
target_entry: &LdapEntry,
user_dn: Option<&str>,
user_entry: Option<&LdapEntry>,
) -> bool {
let cache_key = Self::build_cache_key(operation, &target_entry.dn, user_dn);
if let Some(&allowed) = self.check_cache.get(&cache_key) {
return allowed;
}
let allowed = self
.evaluator
.check_access(operation, target_entry, user_dn, user_entry);
self.check_cache.put(cache_key, allowed);
allowed
}
pub fn authorize(
&mut self,
operation: &LdapOperation,
target_entry: &LdapEntry,
user_dn: Option<&str>,
user_entry: Option<&LdapEntry>,
) -> AuthorizationResult {
let cache_key = Self::build_cache_key(operation, &target_entry.dn, user_dn);
if let Some(&authorized) = self.authorize_cache.get(&cache_key) {
return AuthorizationResult::cached(authorized);
}
let result = self
.evaluator
.authorize(operation, target_entry, user_dn, user_entry);
self.authorize_cache.put(cache_key, result.authorized);
result
}
pub fn authorize_full(
&mut self,
operation: &LdapOperation,
target_entry: &LdapEntry,
user_dn: Option<&str>,
user_entry: Option<&LdapEntry>,
) -> AuthorizationResult {
self.evaluator
.authorize(operation, target_entry, user_dn, user_entry)
}
pub fn check_access_uncached(
&self,
operation: &LdapOperation,
target_entry: &LdapEntry,
user_dn: Option<&str>,
user_entry: Option<&LdapEntry>,
) -> bool {
self.evaluator
.check_access(operation, target_entry, user_dn, user_entry)
}
pub fn clear_cache(&mut self) {
self.check_cache.clear();
self.authorize_cache.clear();
}
pub fn rule_count(&self) -> usize {
self.evaluator.rule_count()
}
pub fn cache_len(&self) -> usize {
self.check_cache.len() + self.authorize_cache.len()
}
fn build_cache_key(
operation: &LdapOperation,
target_dn: &str,
user_dn: Option<&str>,
) -> CacheKey {
use std::hash::{Hash, Hasher};
let mut hasher = ahash::AHasher::default();
(operation.operation_type as u8).hash(&mut hasher);
target_dn.hash(&mut hasher);
user_dn.hash(&mut hasher);
operation.attributes.hash(&mut hasher);
hasher.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aci::AciBuilder;
use crate::aci::BindRule;
use crate::aci::Scope;
use crate::operation::OperationType;
#[test]
fn check_access_cache_hit() {
let acis = vec![AciBuilder::new("read-people")
.target_dn("ou=people,dc=example,dc=com")
.target_scope(Scope::OneLevel)
.permission(OperationType::Read)
.bind_rule(BindRule::Authenticated)
.build()];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("uid=alice,ou=people,dc=example,dc=com");
let op = LdapOperation::new(OperationType::Read, &entry.dn);
let user_dn = Some("uid=bob,ou=people,dc=example,dc=com");
let r1 = cached.check_access(&op, &entry, user_dn, None);
assert_eq!(cached.check_cache.len(), 1);
let r2 = cached.check_access(&op, &entry, user_dn, None);
assert_eq!(r1, r2);
assert_eq!(cached.check_cache.len(), 1);
}
#[test]
fn authorize_cache_hit() {
let acis = vec![AciBuilder::new("read-people")
.target_dn("ou=people,dc=example,dc=com")
.target_scope(Scope::OneLevel)
.permission(OperationType::Read)
.bind_rule(BindRule::Authenticated)
.build()];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("uid=alice,ou=people,dc=example,dc=com");
let op = LdapOperation::new(OperationType::Read, &entry.dn);
let user_dn = Some("uid=bob,ou=people,dc=example,dc=com");
let r1 = cached.authorize(&op, &entry, user_dn, None);
assert_eq!(cached.authorize_cache.len(), 1);
assert!(r1.authorized);
let r2 = cached.authorize(&op, &entry, user_dn, None);
assert_eq!(r1.authorized, r2.authorized);
assert_eq!(cached.authorize_cache.len(), 1);
assert!(r2.grants.is_empty());
assert!(r2.denials.is_empty());
}
#[test]
fn authorize_full_bypasses_cache() {
let acis = vec![AciBuilder::new("read-people")
.target_dn("ou=people,dc=example,dc=com")
.target_scope(Scope::OneLevel)
.permission(OperationType::Read)
.bind_rule(BindRule::Authenticated)
.build()];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("uid=alice,ou=people,dc=example,dc=com");
let op = LdapOperation::new(OperationType::Read, &entry.dn);
let user_dn = Some("uid=bob,ou=people,dc=example,dc=com");
let r1 = cached.authorize_full(&op, &entry, user_dn, None);
assert_eq!(cached.authorize_cache.len(), 0);
assert!(r1.authorized);
assert!(!r1.effective.is_empty());
}
#[test]
fn different_ops_different_keys() {
let acis = vec![AciBuilder::new("read-all")
.permission(OperationType::Read)
.bind_rule(BindRule::Anyone)
.build()];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("dc=example,dc=com");
let read_op = LdapOperation::new(OperationType::Read, &entry.dn);
let write_op = LdapOperation::new(OperationType::Modify, &entry.dn);
cached.check_access(&read_op, &entry, None, None);
cached.check_access(&write_op, &entry, None, None);
assert_eq!(cached.check_cache.len(), 2);
}
#[test]
fn different_attributes_different_keys() {
let acis = vec![
AciBuilder::new("allow-read")
.permission(OperationType::Read)
.bind_rule(BindRule::Anyone)
.build(),
AciBuilder::new("deny-password")
.target_attribute("userpassword")
.permission(OperationType::Read)
.bind_rule(BindRule::Anyone)
.deny()
.build(),
];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("dc=example,dc=com");
let op_cn = LdapOperation::new(OperationType::Read, &entry.dn)
.with_attributes(vec!["cn".to_string()]);
let op_pw = LdapOperation::new(OperationType::Read, &entry.dn)
.with_attributes(vec!["userpassword".to_string()]);
let r1 = cached.check_access(&op_cn, &entry, None, None);
let r2 = cached.check_access(&op_pw, &entry, None, None);
assert_eq!(cached.check_cache.len(), 2);
assert_ne!(
r1, r2,
"cn and userpassword should produce different results"
);
}
#[test]
fn clear_cache_works() {
let acis = vec![AciBuilder::new("read-all")
.permission(OperationType::Read)
.bind_rule(BindRule::Anyone)
.build()];
let mut cached = CachedAciPolicy::new(&acis);
let entry = LdapEntry::new("dc=example,dc=com");
let op = LdapOperation::new(OperationType::Read, &entry.dn);
cached.check_access(&op, &entry, None, None);
cached.authorize(&op, &entry, None, None);
assert_eq!(cached.cache_len(), 2);
cached.clear_cache();
assert_eq!(cached.cache_len(), 0);
}
}