Skip to main content

treetop_core/
labels.rs

1use arc_swap::ArcSwap;
2use regex::Regex;
3use std::sync::Arc;
4
5use crate::types::{AttrValue, Resource};
6
7/// Trait for objects that can label resources based on their attributes.
8///
9/// Implementations should be fast and side-effect free beyond mutating the
10/// provided `Resource`'s attributes. Repeated application must be safe and
11/// idempotent. A labeler owns the attributes it derives: it must replace or
12/// remove preexisting values instead of trusting caller-provided output.
13pub trait Labeler: Send + Sync {
14    /// Returns true if this labeler applies to resources of the given kind.
15    ///
16    /// e.g. "Host", "Database::Table"; you can also support wildcard/globs if you want.
17    fn applies_to(&self, kind: &str) -> bool;
18
19    /// Mutates the resource by injecting derived attributes (e.g., sets of labels).
20    fn apply(&self, res: &mut Resource);
21}
22
23/// A labeler that uses regular expressions for matching on resource attributes.
24#[derive(Debug, Clone)]
25pub struct RegexLabeler {
26    /// The kind of resource this labeler applies to, e.g. "Host"
27    kind: String,
28    /// attribute to read from, e.g. "name"
29    field: String,
30    /// attribute to write to, e.g. "nameLabels"
31    output: String,
32    /// Rulesets for matching resource attributes
33    table: Vec<(String, Regex)>,
34}
35
36impl RegexLabeler {
37    /// Create a regex-based labeler.
38    ///
39    /// - `kind`: resource kind this applies to (e.g., "Host")
40    /// - `field`: attribute to read from (e.g., "name")
41    /// - `output`: attribute to write labels to (e.g., "nameLabels")
42    /// - `table`: vector of `(label, regex)` pairs
43    ///
44    /// Configure `field` and `output` as distinct attributes so repeated
45    /// application remains idempotent. `field` reads the resource attribute
46    /// map; it does not expose canonical entity fields. In particular, an
47    /// attribute named `id` is not the canonical [`Resource::id`] value during
48    /// labeling. Use a custom [`Labeler`] that reads [`Resource::id`] when
49    /// labels must derive from the resource identity.
50    pub fn new(
51        kind: impl Into<String>,
52        field: impl Into<String>,
53        output: impl Into<String>,
54        table: Vec<(String, Regex)>,
55    ) -> Self {
56        Self {
57            kind: kind.into(),
58            field: field.into(),
59            output: output.into(),
60            table,
61        }
62    }
63}
64
65impl Labeler for RegexLabeler {
66    fn applies_to(&self, kind: &str) -> bool {
67        self.kind == kind
68    }
69
70    fn apply(&self, res: &mut Resource) {
71        let Some(AttrValue::String(value)) = res.attributes().get(&self.field) else {
72            // The output is derived and therefore must never preserve a value
73            // supplied by the caller when its trusted input is unavailable.
74            res.attrs().remove(&self.output);
75            return;
76        };
77        let out = self
78            .table
79            .iter()
80            .filter(|(_, re)| re.is_match(value))
81            .map(|(label, _)| AttrValue::String(label.clone()))
82            .collect();
83
84        // Replace even when the result is empty. Retaining or extending a
85        // caller-provided set would make a derived authorization label
86        // forgeable.
87        res.attrs().insert(self.output.clone(), AttrValue::Set(out));
88    }
89}
90
91/// Implementation of the LabelRegistry.
92///
93/// Consumption of this registry goes through the static `LABEL_REGISTRY`.
94pub struct LabelRegistry {
95    inner: ArcSwap<Vec<Arc<dyn Labeler>>>,
96}
97impl LabelRegistry {
98    /// Clone and label a resource only when at least one labeler applies.
99    ///
100    /// The first matching labeler is found before cloning so registries that
101    /// serve other resource kinds add no resource-clone cost. Each labeler's
102    /// applicability predicate is still evaluated at most once and labelers
103    /// retain insertion order.
104    pub(crate) fn apply_to_clone_if_applicable(&self, res: &Resource) -> Option<Resource> {
105        let snapshot = self.inner.load();
106        let first_match = snapshot
107            .iter()
108            .position(|labeler| labeler.applies_to(res.kind()))?;
109
110        let mut labelled = res.clone();
111        snapshot[first_match].apply(&mut labelled);
112        for labeler in &snapshot[first_match + 1..] {
113            if labeler.applies_to(labelled.kind()) {
114                labeler.apply(&mut labelled);
115            }
116        }
117        Some(labelled)
118    }
119
120    /// Applies all labelers in the registry to the given resource.
121    ///
122    /// Labelers run in insertion order. Each labeler owns its derived output;
123    /// if multiple labelers target the same attribute, the last one wins.
124    pub fn apply(&self, res: &mut Resource) {
125        let snapshot = self.inner.load();
126        for l in snapshot.iter() {
127            if l.applies_to(res.kind()) {
128                l.apply(res);
129            }
130        }
131    }
132
133    /// Loads a set of labelers into the registry, atomically.
134    ///
135    /// Replaces all prior labelers in a single swap. New `evaluate()` calls use
136    /// the new set immediately; in-flight evaluations continue with the old set.
137    pub fn reload(&self, labelers: Vec<Arc<dyn Labeler>>) {
138        self.inner.store(Arc::new(labelers));
139    }
140}
141
142/// Builder for creating a LabelRegistry with labelers.
143///
144/// This uses a builder pattern to ensure labelers are properly initialized
145/// before the registry is used.
146///
147/// # Example
148///
149/// ```rust
150/// use std::sync::Arc;
151/// use treetop_core::{LabelRegistryBuilder, RegexLabeler};
152/// use regex::Regex;
153///
154/// let registry = LabelRegistryBuilder::new()
155///     .add_labeler(Arc::new(RegexLabeler::new(
156///         "Host",
157///         "name",
158///         "nameLabels",
159///         vec![("prod".to_string(), Regex::new(r"\.prod\.").unwrap())],
160///     )))
161///     .build();
162/// ```
163pub struct LabelRegistryBuilder {
164    labelers: Vec<Arc<dyn Labeler>>,
165}
166
167impl LabelRegistryBuilder {
168    /// Create a new, empty label registry builder.
169    pub fn new() -> Self {
170        Self {
171            labelers: Vec::new(),
172        }
173    }
174
175    /// Add a labeler to the registry.
176    ///
177    /// This can be called repeatedly to build up a registry before `build()`.
178    pub fn add_labeler(mut self, labeler: Arc<dyn Labeler>) -> Self {
179        self.labelers.push(labeler);
180        self
181    }
182
183    /// Build the label registry.
184    ///
185    /// Consumes the builder and returns an initialized registry ready to use
186    /// with `PolicyEngine::with_label_registry()`.
187    pub fn build(self) -> LabelRegistry {
188        LabelRegistry {
189            inner: ArcSwap::from_pointee(self.labelers),
190        }
191    }
192}
193
194impl Default for LabelRegistryBuilder {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::collections::BTreeSet;
204    use yare::parameterized;
205
206    fn compile(rules: Vec<(&str, &str)>) -> Vec<(String, Regex)> {
207        rules
208            .into_iter()
209            .map(|(l, p)| (l.to_string(), Regex::new(p).unwrap()))
210            .collect()
211    }
212
213    fn get_label_strings(res: &mut Resource, key: &str) -> BTreeSet<String> {
214        match res.attrs().get(key) {
215            Some(AttrValue::Set(v)) => v
216                .iter()
217                .filter_map(|a| {
218                    if let AttrValue::String(s) = a {
219                        Some(s.clone())
220                    } else {
221                        None
222                    }
223                })
224                .collect(),
225            _ => BTreeSet::new(),
226        }
227    }
228
229    #[parameterized(
230        simple_match = {
231            "Host", "name", "nameLabels",
232            vec![("prod", r"(^|\.)prod\.example\.com$")],
233            "db12.prod.example.com",
234            &["prod"]
235        },
236        no_match = {
237            "Host", "name", "nameLabels",
238            vec![("corp", r"(^|\.)corp\.example\.com$")],
239            "web.dev.example.com",
240            &[]
241        },
242        multi_match = {
243            "Host", "name", "nameLabels",
244            vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")],
245            "db42.prod.example.com",
246            &["db","prod"]
247        }
248    )]
249    fn regex_labeler_apply_basic(
250        kind: &str,
251        field: &str,
252        output: &str,
253        rules: Vec<(&str, &str)>,
254        input: &str,
255        expected: &[&str],
256    ) {
257        let labeler = RegexLabeler::new(kind, field, output, compile(rules));
258
259        let mut res = Resource::new(kind, input);
260        res.attrs()
261            .insert(field.to_string(), AttrValue::String(input.to_string()));
262
263        labeler.apply(&mut res);
264
265        let got = get_label_strings(&mut res, output);
266        let want: BTreeSet<String> = expected.iter().map(|s| s.to_string()).collect();
267        assert_eq!(got, want);
268    }
269
270    #[test]
271    fn regex_labeler_missing_input_field_is_noop() {
272        let labeler = RegexLabeler::new(
273            "Host",
274            "name",
275            "nameLabels",
276            compile(vec![("prod", r"(^|\.)prod\.")]),
277        );
278
279        let mut res = Resource::new("Host", "db99.prod.example.com");
280        // no "name" inserted
281
282        labeler.apply(&mut res);
283        assert!(res.attrs().get("nameLabels").is_none());
284    }
285
286    #[test]
287    fn regex_labeler_replaces_untrusted_existing_set() {
288        let labeler = RegexLabeler::new(
289            "Host",
290            "name",
291            "nameLabels",
292            compile(vec![("prod", r"(^|\.)prod\."), ("db", r"(^|\.)db\d+\.")]),
293        );
294
295        let mut res = Resource::new("Host", "db99.prod.example.com");
296        res.attrs().insert(
297            "name".into(),
298            AttrValue::String("db99.prod.example.com".into()),
299        );
300        res.attrs().insert(
301            "nameLabels".into(),
302            AttrValue::Set(vec![AttrValue::String("pre".into())]),
303        );
304
305        labeler.apply(&mut res);
306
307        let labels = get_label_strings(&mut res, "nameLabels");
308        assert!(!labels.contains("pre"));
309        assert!(labels.contains("prod"));
310        assert!(labels.contains("db"));
311    }
312
313    #[test]
314    fn regex_labeler_replaces_untrusted_set_when_no_rule_matches() {
315        let labeler = RegexLabeler::new(
316            "Host",
317            "name",
318            "nameLabels",
319            compile(vec![("prod", r"(^|\.)prod\.")]),
320        );
321        let mut res = Resource::new("Host", "public.example.com")
322            .with_attr("name", AttrValue::String("public.example.com".into()))
323            .with_attr(
324                "nameLabels",
325                AttrValue::Set(vec![AttrValue::String("prod".into())]),
326            );
327
328        labeler.apply(&mut res);
329
330        assert_eq!(
331            res.attributes().get("nameLabels"),
332            Some(&AttrValue::Set(Vec::new()))
333        );
334    }
335
336    #[test]
337    fn regex_labeler_removes_untrusted_output_when_input_is_missing() {
338        let labeler = RegexLabeler::new(
339            "Host",
340            "name",
341            "nameLabels",
342            compile(vec![("prod", r"(^|\.)prod\.")]),
343        );
344        let mut res = Resource::new("Host", "public.example.com").with_attr(
345            "nameLabels",
346            AttrValue::Set(vec![AttrValue::String("prod".into())]),
347        );
348
349        labeler.apply(&mut res);
350
351        assert!(!res.attributes().contains_key("nameLabels"));
352    }
353}