1use {
10 crate::{AclEntries, AclSource},
11 krabka_metadata::{AclEntry, PatternType, ResourceType},
12 std::{collections::HashMap, iter},
13};
14
15const WILDCARD_RESOURCE: &str = "*";
18
19#[derive(Debug, Clone, Default)]
21pub struct AclCache {
22 entries: Vec<AclEntry>,
23 by_type: HashMap<ResourceType, TypeIndex>,
24 cidr_hosts_supported: bool,
25}
26
27#[derive(Debug, Clone, Default)]
29struct TypeIndex {
30 all: Vec<usize>,
31 literal: HashMap<String, Vec<usize>>,
32 prefixed: HashMap<String, Vec<usize>>,
33 prefix_lens: Vec<usize>,
35}
36
37impl TypeIndex {
38 fn literal(&self, name: &str) -> impl Iterator<Item = usize> + '_ {
39 self.literal.get(name).into_iter().flatten().copied()
40 }
41
42 fn prefixed<'a>(&'a self, name: &'a str) -> impl Iterator<Item = usize> + 'a {
46 self.prefix_lens
47 .iter()
48 .take_while(move |&&len| len <= name.len())
49 .filter_map(move |&len| name.get(..len))
50 .filter_map(|prefix| self.prefixed.get(prefix))
51 .flatten()
52 .copied()
53 }
54}
55
56impl AclCache {
57 #[must_use]
61 pub fn new(entries: Vec<AclEntry>) -> Self {
62 let mut by_type: HashMap<ResourceType, TypeIndex> = HashMap::new();
63 for (position, entry) in entries.iter().enumerate() {
64 let index = by_type.entry(entry.resource_type).or_default();
65 index.all.push(position);
66 let names = match entry.pattern_type {
67 PatternType::Literal => &mut index.literal,
68 PatternType::Prefixed => &mut index.prefixed,
69 };
70 names
71 .entry(entry.resource_name.clone())
72 .or_default()
73 .push(position);
74 }
75 for index in by_type.values_mut() {
76 index.prefix_lens = index.prefixed.keys().map(String::len).collect();
77 index.prefix_lens.sort_unstable();
78 index.prefix_lens.dedup();
79 }
80 Self {
81 entries,
82 by_type,
83 cidr_hosts_supported: false,
84 }
85 }
86
87 #[must_use]
93 pub fn with_cidr_hosts_supported(mut self, supported: bool) -> Self {
94 self.cidr_hosts_supported = supported;
95 self
96 }
97
98 #[must_use]
99 pub fn len(&self) -> usize { self.entries.len() }
100
101 #[must_use]
102 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
103
104 fn positions<'a>(&'a self, positions: impl Iterator<Item = usize> + 'a) -> AclEntries<'a> {
105 Box::new(positions.filter_map(|position| self.entries.get(position)))
106 }
107}
108
109impl AclSource for AclCache {
110 fn matching_acls<'a>(&'a self, rt: ResourceType, name: &'a str) -> AclEntries<'a> {
111 let Some(index) = self.by_type.get(&rt) else {
116 return Box::new(iter::empty());
117 };
118 let wildcard = (name != WILDCARD_RESOURCE)
119 .then(|| index.literal(WILDCARD_RESOURCE))
120 .into_iter()
121 .flatten();
122 self.positions(
123 index
124 .literal(name)
125 .chain(wildcard)
126 .chain(index.prefixed(name)),
127 )
128 }
129
130 fn acls_of_type(&self, rt: ResourceType) -> AclEntries<'_> {
131 self.positions(
132 self.by_type
133 .get(&rt)
134 .into_iter()
135 .flat_map(|index| index.all.iter().copied()),
136 )
137 }
138
139 fn cidr_hosts_supported(&self) -> bool { self.cidr_hosts_supported }
140}
141
142#[cfg(test)]
143mod tests {
144
145 use {
146 super::*,
147 crate::{AuthorizationResult, test_support::acl as entry},
148 krabka_metadata::{
149 AclOperation, MetadataImage, MetadataRecord, ResourceType,
150 metadata_version::CIDR_ACL_MIN_LEVEL,
151 },
152 proptest::{collection, strategy::Strategy},
153 std::collections::HashSet,
154 uuid::Uuid,
155 };
156
157 fn key(e: &AclEntry) -> String {
164 format!(
165 "{:?}|{:?}|{}|{:?}",
166 e.resource_type, e.pattern_type, e.resource_name, e.operation
167 )
168 }
169
170 fn sorted_keys(it: AclEntries<'_>) -> Vec<String> {
171 let mut v: Vec<_> = it.map(key).collect();
172 v.sort();
173 v
174 }
175
176 #[test]
183 fn cache_matches_image_for_every_probe() {
184 let entries = vec![
187 entry().call(),
188 entry().op(AclOperation::Write).name("*").call(),
189 entry().pattern(PatternType::Prefixed).name("team-").call(),
190 entry().name("bar").call(),
191 entry().rt(ResourceType::Group).name("cg-1").call(),
192 entry()
193 .rt(ResourceType::Group)
194 .pattern(PatternType::Prefixed)
195 .name("app-")
196 .call(),
197 ];
198
199 let mut image = MetadataImage::new(Uuid::nil());
201 for e in &entries {
202 image.apply(&MetadataRecord::V1AccessControlEntry(e.clone()));
203 }
204 let cache = AclCache::new(entries);
206
207 let probes = [
209 (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"), ];
219
220 for (rt, name) in probes {
221 let from_image = sorted_keys(AclSource::matching_acls(&image, rt, name));
222 let from_cache = sorted_keys(AclSource::matching_acls(&cache, rt, name));
223 assert2::assert!(from_image == from_cache);
224 }
225 }
226
227 #[yare::parameterized(
235 unfinalized_version = { None, Some(false), AuthorizationResult::Deny },
236 below_range_level = {
237 Some(CIDR_ACL_MIN_LEVEL - 1),
238 Some(false),
239 AuthorizationResult::Deny,
240 },
241 minimum_range_level = {
242 Some(CIDR_ACL_MIN_LEVEL),
243 Some(true),
244 AuthorizationResult::Allow,
245 },
246 unconfigured_cache = { None, None, AuthorizationResult::Deny },
247 )]
248 fn a_cache_reads_a_slash_host_as_the_cluster_does(
249 level: Option<i16>,
250 cache_support: Option<bool>,
251 expected: AuthorizationResult,
252 ) {
253 use {
254 crate::{AuthorizationRequest, Authorizer, SimpleAclAuthorizer},
255 krabka_metadata::{FeatureLevelRecord, metadata_version::METADATA_VERSION_FEATURE},
256 krabka_security::{AuthMethod, Principal},
257 };
258
259 let range_allow = AclEntry {
260 host: "10.0.0.0/8".into(),
261 ..entry().call()
262 };
263 let alice = Principal {
264 name: "alice".into(),
265 auth_method: AuthMethod::SaslPlain,
266 groups: vec![],
267 };
268 let peer = "10.1.2.3:5000".parse().unwrap();
269 let request = AuthorizationRequest {
270 principal: &alice,
271 host: &peer,
272 resource_type: ResourceType::Topic,
273 resource_name: "foo",
274 operation: AclOperation::Read,
275 };
276 let auth = SimpleAclAuthorizer::new(HashSet::new());
277 let mut image = MetadataImage::new(Uuid::nil());
278 if let Some(level) = level {
279 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
280 name: METADATA_VERSION_FEATURE.into(),
281 level,
282 }));
283 }
284 image.apply(&MetadataRecord::V1AccessControlEntry(range_allow.clone()));
285 let cache = match cache_support {
286 Some(supported) => {
287 AclCache::new(vec![range_allow]).with_cidr_hosts_supported(supported)
288 }
289 None => AclCache::new(vec![range_allow]),
290 };
291 assert2::assert!(
292 (
293 auth.authorize(&image, &request),
294 auth.authorize(&cache, &request)
295 ) == (expected, expected)
296 );
297 }
298
299 fn scanned(entries: &[AclEntry], rt: ResourceType, name: &str) -> Vec<String> {
302 let mut v: Vec<_> = entries
303 .iter()
304 .filter(|e| {
305 e.resource_type == rt
306 && match e.pattern_type {
307 PatternType::Literal => e.resource_name == name || e.resource_name == "*",
308 PatternType::Prefixed => name.starts_with(e.resource_name.as_str()),
309 }
310 })
311 .map(key)
312 .collect();
313 v.sort();
314 v
315 }
316
317 fn arb_entry() -> impl Strategy<Value = AclEntry> {
318 use proptest::prelude::*;
319 (
320 prop_oneof![Just(ResourceType::Topic), Just(ResourceType::Group)],
321 prop_oneof![Just(PatternType::Literal), Just(PatternType::Prefixed)],
322 prop_oneof![Just(AclOperation::Read), Just(AclOperation::Write)],
323 "(\\*|[aé-]{0,4})",
324 )
325 .prop_map(|(rt, pattern, op, name)| {
326 entry().rt(rt).pattern(pattern).op(op).name(&name).call()
327 })
328 }
329
330 proptest::proptest! {
331 #[test]
336 fn index_matches_a_full_scan(
337 entries in collection::vec(arb_entry(), 0..24),
338 probes in collection::vec("(\\*|[aé-]{0,6})", 1..8),
339 ) {
340 let cache = AclCache::new(entries.clone());
341 for name in &probes {
342 for rt in [ResourceType::Topic, ResourceType::Group, ResourceType::Cluster] {
343 let indexed = sorted_keys(AclSource::matching_acls(&cache, rt, name));
344 assert2::assert!(indexed == scanned(&entries, rt, name));
345 let mut of_type: Vec<_> = cache.acls_of_type(rt).map(key).collect();
346 of_type.sort();
347 let mut expected: Vec<_> =
348 entries.iter().filter(|e| e.resource_type == rt).map(key).collect();
349 expected.sort();
350 assert2::assert!(of_type == expected);
351 }
352 }
353 }
354 }
355
356 #[test]
359 fn every_applicable_prefix_length_is_found() {
360 let cache = AclCache::new(vec![
361 entry().pattern(PatternType::Prefixed).name("t").call(),
362 entry().pattern(PatternType::Prefixed).name("team-").call(),
363 entry().pattern(PatternType::Prefixed).name("team-a").call(),
364 entry()
365 .pattern(PatternType::Prefixed)
366 .name("team-a-long")
367 .call(),
368 entry().pattern(PatternType::Prefixed).name("other").call(),
369 ]);
370 let names: Vec<_> = AclSource::matching_acls(&cache, ResourceType::Topic, "team-a1")
371 .map(|e| e.resource_name.as_str())
372 .collect();
373 assert2::assert!(names == ["t", "team-", "team-a"]);
374 }
375
376 #[test]
377 fn len_and_is_empty_track_entries() {
378 let empty = AclCache::default();
379 assert2::assert!(empty.is_empty());
380 assert2::assert!(empty.len() == 0);
381
382 let cache = AclCache::new(vec![entry().call()]);
383 assert2::assert!(!cache.is_empty());
384 assert2::assert!(cache.len() == 1);
385 }
386}