use crabka_metadata::{AclEntry, PatternType, ResourceType};
use crate::AclSource;
#[derive(Debug, Clone, Default)]
pub struct AclCache {
entries: Vec<AclEntry>,
}
impl AclCache {
#[must_use]
pub fn new(entries: Vec<AclEntry>) -> Self {
Self { entries }
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl AclSource for AclCache {
fn matching_acls<'a>(
&'a self,
rt: ResourceType,
name: &'a str,
) -> Box<dyn Iterator<Item = &'a AclEntry> + 'a> {
Box::new(self.entries.iter().filter(move |e| {
e.resource_type == rt
&& match e.pattern_type {
PatternType::Literal => e.resource_name == name || e.resource_name == "*",
PatternType::Prefixed => name.starts_with(e.resource_name.as_str()),
}
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::assert;
use crabka_metadata::{
AclOperation, MetadataImage, MetadataRecord, PermissionType, ResourceType,
};
use uuid::Uuid;
fn entry(rt: ResourceType, pattern: PatternType, name: &str, op: AclOperation) -> AclEntry {
AclEntry {
resource_type: rt,
resource_name: name.into(),
pattern_type: pattern,
principal: "User:alice".into(),
host: "*".into(),
operation: op,
permission_type: PermissionType::Allow,
}
}
fn key(e: &AclEntry) -> String {
format!(
"{:?}|{:?}|{}|{:?}",
e.resource_type, e.pattern_type, e.resource_name, e.operation
)
}
fn sorted_keys<'a>(it: Box<dyn Iterator<Item = &'a AclEntry> + 'a>) -> Vec<String> {
let mut v: Vec<_> = it.map(key).collect();
v.sort();
v
}
#[test]
fn cache_matches_image_for_every_probe() {
let entries = vec![
entry(
ResourceType::Topic,
PatternType::Literal,
"foo",
AclOperation::Read,
),
entry(
ResourceType::Topic,
PatternType::Literal,
"*",
AclOperation::Write,
),
entry(
ResourceType::Topic,
PatternType::Prefixed,
"team-",
AclOperation::Read,
),
entry(
ResourceType::Topic,
PatternType::Literal,
"bar",
AclOperation::Read,
),
entry(
ResourceType::Group,
PatternType::Literal,
"cg-1",
AclOperation::Read,
),
entry(
ResourceType::Group,
PatternType::Prefixed,
"app-",
AclOperation::Read,
),
];
let mut image = MetadataImage::new(Uuid::nil());
for e in &entries {
image.apply(&MetadataRecord::V1AccessControlEntry(e.clone()));
}
let cache = AclCache::new(entries.clone());
let probes: &[(ResourceType, &str)] = &[
(ResourceType::Topic, "foo"), (ResourceType::Topic, "*"), (ResourceType::Topic, "team-foo"), (ResourceType::Topic, "team-"), (ResourceType::Topic, "nomatch"), (ResourceType::Group, "cg-1"), (ResourceType::Group, "app-svc"), (ResourceType::Group, "other"), (ResourceType::Cluster, "kafka-cluster"), ];
for &(rt, name) in probes {
let from_image = sorted_keys(AclSource::matching_acls(&image, rt, name));
let from_cache = sorted_keys(AclSource::matching_acls(&cache, rt, name));
assert!(
from_image == from_cache,
"drift at ({rt:?}, {name:?}): image={from_image:?} cache={from_cache:?}"
);
}
}
#[test]
fn len_and_is_empty_track_entries() {
let empty = AclCache::default();
assert!(empty.is_empty());
assert!(empty.len() == 0);
let cache = AclCache::new(vec![entry(
ResourceType::Topic,
PatternType::Literal,
"foo",
AclOperation::Read,
)]);
assert!(!cache.is_empty());
assert!(cache.len() == 1);
}
}