1use crate::{BundleError, Diagnostic, Result};
2use cedar_policy::{EntityTypeName, Schema};
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashSet;
7use std::sync::Arc;
8use treetop_core::{Labeler, RegexLabeler};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12struct RawLabelPattern {
13 name: String,
14 regex: String,
15}
16
17#[derive(Debug, Clone, Serialize)]
19pub struct LabelPattern {
20 name: String,
21 regex: String,
22 #[serde(skip)]
23 compiled: Regex,
24}
25
26impl PartialEq for LabelPattern {
27 fn eq(&self, other: &Self) -> bool {
28 self.name == other.name && self.regex == other.regex
29 }
30}
31
32impl Eq for LabelPattern {}
33
34impl LabelPattern {
35 pub fn name(&self) -> &str {
36 &self.name
37 }
38
39 pub fn regex(&self) -> &str {
40 &self.regex
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46struct RawLabelRule {
47 kind: String,
48 field: String,
49 output: String,
50 patterns: Vec<RawLabelPattern>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct LabelRule {
56 kind: String,
57 field: String,
58 output: String,
59 patterns: Vec<LabelPattern>,
60}
61
62impl LabelRule {
63 pub fn kind(&self) -> &str {
64 &self.kind
65 }
66
67 pub fn field(&self) -> &str {
68 &self.field
69 }
70
71 pub fn output(&self) -> &str {
72 &self.output
73 }
74
75 pub fn patterns(&self) -> &[LabelPattern] {
76 &self.patterns
77 }
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
82#[serde(transparent)]
83pub struct LabelSet(Vec<LabelRule>);
84
85impl LabelSet {
86 pub fn from_json_str(input: &str) -> Result<Self> {
88 let raw: Vec<RawLabelRule> = serde_json::from_str(input).map_err(|error| {
89 let mut diagnostic = Diagnostic::error("labels.invalid_json", error.to_string());
90 diagnostic.line = Some(error.line());
91 diagnostic.column = Some(error.column());
92 BundleError::Validation(vec![diagnostic])
93 })?;
94 Self::from_raw(raw)
95 }
96
97 pub fn validate_schema_json_str(&self, schema_source: &str) -> Result<()> {
99 let schema_json: Value = serde_json::from_str(schema_source).map_err(|error| {
100 BundleError::Validation(vec![Diagnostic::error(
101 "schema.invalid_json",
102 error.to_string(),
103 )])
104 })?;
105 let schema = Schema::from_json_value(schema_json.clone()).map_err(|error| {
106 BundleError::Validation(vec![Diagnostic::error(
107 "schema.aggregate_invalid",
108 error.to_string(),
109 )])
110 })?;
111 let diagnostics = self.validate_schema(&schema, &schema_json);
112 if diagnostics.is_empty() {
113 Ok(())
114 } else {
115 Err(BundleError::Validation(diagnostics))
116 }
117 }
118
119 fn from_raw(raw: Vec<RawLabelRule>) -> Result<Self> {
120 let mut diagnostics = Vec::new();
121 let mut destinations = HashSet::new();
122 let mut rules = Vec::with_capacity(raw.len());
123
124 for (rule_index, raw_rule) in raw.into_iter().enumerate() {
125 let location = format!("labels[{rule_index}]");
126 if raw_rule.kind.trim().is_empty() {
127 diagnostics.push(Diagnostic::error(
128 "labels.empty_kind",
129 format!("{location}.kind must not be empty"),
130 ));
131 } else if raw_rule.kind.parse::<EntityTypeName>().is_err() {
132 diagnostics.push(Diagnostic::error(
133 "labels.invalid_kind",
134 format!("{location}.kind is not a Cedar entity type"),
135 ));
136 }
137 if raw_rule.field.trim().is_empty() {
138 diagnostics.push(Diagnostic::error(
139 "labels.empty_field",
140 format!("{location}.field must not be empty"),
141 ));
142 }
143 if raw_rule.output.trim().is_empty() {
144 diagnostics.push(Diagnostic::error(
145 "labels.empty_output",
146 format!("{location}.output must not be empty"),
147 ));
148 }
149 if raw_rule.field == raw_rule.output {
150 diagnostics.push(Diagnostic::error(
151 "labels.input_is_output",
152 format!("{location}.field and output must be different"),
153 ));
154 }
155 if !destinations.insert((raw_rule.kind.clone(), raw_rule.output.clone())) {
156 diagnostics.push(Diagnostic::error(
157 "labels.duplicate_destination",
158 format!(
159 "duplicate label destination ({}, {})",
160 raw_rule.kind, raw_rule.output
161 ),
162 ));
163 }
164 if raw_rule.patterns.is_empty() {
165 diagnostics.push(Diagnostic::error(
166 "labels.empty_patterns",
167 format!("{location}.patterns must not be empty"),
168 ));
169 }
170
171 let mut names = HashSet::new();
172 let mut patterns = Vec::with_capacity(raw_rule.patterns.len());
173 for (pattern_index, raw_pattern) in raw_rule.patterns.into_iter().enumerate() {
174 let pattern_location = format!("{location}.patterns[{pattern_index}]");
175 if raw_pattern.name.trim().is_empty() {
176 diagnostics.push(Diagnostic::error(
177 "labels.empty_pattern_name",
178 format!("{pattern_location}.name must not be empty"),
179 ));
180 }
181 if !names.insert(raw_pattern.name.clone()) {
182 diagnostics.push(Diagnostic::error(
183 "labels.duplicate_pattern_name",
184 format!(
185 "duplicate pattern name {:?} in {location}",
186 raw_pattern.name
187 ),
188 ));
189 }
190 let compiled = if raw_pattern.regex.is_empty() {
191 diagnostics.push(Diagnostic::error(
192 "labels.empty_regex",
193 format!("{pattern_location}.regex must not be empty"),
194 ));
195 None
196 } else {
197 match Regex::new(&raw_pattern.regex) {
198 Ok(regex) => Some(regex),
199 Err(error) => {
200 diagnostics.push(Diagnostic::error(
201 "labels.invalid_regex",
202 format!("{pattern_location}.regex is invalid: {error}"),
203 ));
204 None
205 }
206 }
207 };
208 if let Some(compiled) = compiled {
209 patterns.push(LabelPattern {
210 name: raw_pattern.name,
211 regex: raw_pattern.regex,
212 compiled,
213 });
214 }
215 }
216
217 rules.push(LabelRule {
218 kind: raw_rule.kind,
219 field: raw_rule.field,
220 output: raw_rule.output,
221 patterns,
222 });
223 }
224
225 if diagnostics.is_empty() {
226 Ok(Self(rules))
227 } else {
228 Err(BundleError::Validation(diagnostics))
229 }
230 }
231
232 pub(crate) fn combine(sets: impl IntoIterator<Item = Self>) -> Result<Self> {
233 let rules = sets.into_iter().flat_map(|set| set.0).collect::<Vec<_>>();
234 let mut destinations = HashSet::with_capacity(rules.len());
235 let mut diagnostics = Vec::new();
236 for rule in &rules {
237 if !destinations.insert((&rule.kind, &rule.output)) {
238 diagnostics.push(Diagnostic::error(
239 "labels.duplicate_destination",
240 format!(
241 "duplicate label destination ({}, {})",
242 rule.kind, rule.output
243 ),
244 ));
245 }
246 }
247 if diagnostics.is_empty() {
248 Ok(Self(rules))
249 } else {
250 Err(BundleError::Validation(diagnostics))
251 }
252 }
253
254 pub fn rules(&self) -> &[LabelRule] {
255 &self.0
256 }
257
258 pub fn is_empty(&self) -> bool {
259 self.0.is_empty()
260 }
261
262 pub fn to_labelers(&self) -> Vec<Arc<dyn Labeler>> {
264 self.0
265 .iter()
266 .map(|rule| {
267 let patterns = rule
268 .patterns
269 .iter()
270 .map(|pattern| (pattern.name.clone(), pattern.compiled.clone()))
271 .collect();
272 Arc::new(RegexLabeler::new(
273 rule.kind.clone(),
274 rule.field.clone(),
275 rule.output.clone(),
276 patterns,
277 )) as Arc<dyn Labeler>
278 })
279 .collect()
280 }
281
282 pub(crate) fn validate_schema(&self, schema: &Schema, schema_json: &Value) -> Vec<Diagnostic> {
283 let known_types = schema
284 .entity_types()
285 .map(ToString::to_string)
286 .collect::<HashSet<_>>();
287 let mut diagnostics = Vec::new();
288 for rule in &self.0 {
289 if !known_types.contains(&rule.kind) {
290 diagnostics.push(Diagnostic::error(
291 "labels.unknown_kind",
292 format!("label kind {} is not declared in the schema", rule.kind),
293 ));
294 continue;
295 }
296 let Some(attributes) = entity_attributes(schema_json, &rule.kind) else {
297 diagnostics.push(Diagnostic::error(
298 "labels.missing_shape",
299 format!("label kind {} has no record shape", rule.kind),
300 ));
301 continue;
302 };
303 match attributes.get(&rule.field) {
304 Some(value) if is_string_type(value) => {}
305 Some(value) => diagnostics.push(Diagnostic::error(
306 "labels.field_not_string",
307 format!(
308 "{}.{} must have schema type String, found {value}",
309 rule.kind, rule.field
310 ),
311 )),
312 None => diagnostics.push(Diagnostic::error(
313 "labels.field_missing",
314 format!("{}.{} is not declared in the schema", rule.kind, rule.field),
315 )),
316 }
317 match attributes.get(&rule.output) {
318 Some(value) if is_string_set_type(value) => {}
319 Some(value) => diagnostics.push(Diagnostic::error(
320 "labels.output_not_string_set",
321 format!(
322 "{}.{} must have schema type Set<String>, found {value}",
323 rule.kind, rule.output,
324 ),
325 )),
326 None => diagnostics.push(Diagnostic::error(
327 "labels.output_missing",
328 format!(
329 "{}.{} is not declared in the schema",
330 rule.kind, rule.output
331 ),
332 )),
333 }
334 }
335 diagnostics
336 }
337}
338
339fn entity_attributes<'a>(
340 schema_json: &'a Value,
341 kind: &str,
342) -> Option<&'a serde_json::Map<String, Value>> {
343 let parsed = kind.parse::<EntityTypeName>().ok()?;
344 let namespace = parsed.namespace().to_string();
345 let namespace_definition = schema_json.as_object()?.get(&namespace)?;
346 let definition = namespace_definition
347 .get("entityTypes")?
348 .get(parsed.basename())?;
349 record_attributes(
350 schema_json,
351 &namespace,
352 definition.get("shape")?,
353 &mut HashSet::new(),
354 )
355}
356
357fn record_attributes<'a>(
358 schema_json: &'a Value,
359 namespace: &str,
360 shape: &'a Value,
361 visited: &mut HashSet<String>,
362) -> Option<&'a serde_json::Map<String, Value>> {
363 if shape.get("type").and_then(Value::as_str) == Some("Record") {
364 return shape.get("attributes")?.as_object();
365 }
366 if shape.get("type").and_then(Value::as_str) != Some("EntityOrCommon") {
367 return None;
368 }
369 let name = shape.get("name")?.as_str()?;
370 let (common_namespace, basename) = name
371 .rsplit_once("::")
372 .map_or((namespace, name), |(namespace, basename)| {
373 (namespace, basename)
374 });
375 let qualified_name = format!("{common_namespace}::{basename}");
376 if !visited.insert(qualified_name) {
377 return None;
378 }
379 let common = schema_json
380 .as_object()?
381 .get(common_namespace)?
382 .get("commonTypes")?
383 .get(basename)?;
384 record_attributes(schema_json, common_namespace, common, visited)
385}
386
387fn is_string_type(value: &Value) -> bool {
388 value.get("type").and_then(Value::as_str) == Some("String")
389 || (value.get("type").and_then(Value::as_str) == Some("EntityOrCommon")
390 && value.get("name").and_then(Value::as_str) == Some("String"))
391}
392
393fn is_string_set_type(value: &Value) -> bool {
394 value.get("type").and_then(Value::as_str) == Some("Set")
395 && value.get("element").is_some_and(is_string_type)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn strict_label_validation_rejects_unknown_fields() {
404 let error = LabelSet::from_json_str(
405 r#"[{"kind":"App::Host","field":"name","output":"labels","patterns":[{"name":"prod","regex":"prod","extra":true}]}]"#,
406 )
407 .unwrap_err();
408 assert!(error.diagnostics()[0].message.contains("unknown field"));
409 }
410
411 #[test]
412 fn label_set_converts_to_runtime_labelers() {
413 let labels = LabelSet::from_json_str(
414 r#"[{"kind":"App::Host","field":"name","output":"labels","patterns":[{"name":"prod","regex":"^prod"}]}]"#,
415 )
416 .unwrap();
417 assert_eq!(labels.to_labelers().len(), 1);
418 }
419
420 #[test]
421 fn schema_validation_resolves_common_record_shapes() {
422 let labels = LabelSet::from_json_str(
423 r#"[{"kind":"App::Host","field":"name","output":"labels","patterns":[{"name":"prod","regex":"^prod"}]}]"#,
424 )
425 .unwrap();
426 let schema = r#"{
427 "App": {
428 "commonTypes": {
429 "HostShape": {
430 "type": "Record",
431 "attributes": {
432 "name": {"type": "String", "required": true},
433 "labels": {
434 "type": "Set",
435 "element": {"type": "String"},
436 "required": false
437 }
438 },
439 "additionalAttributes": false
440 }
441 },
442 "entityTypes": {
443 "Host": {
444 "shape": {"type": "EntityOrCommon", "name": "HostShape"}
445 }
446 },
447 "actions": {}
448 }
449 }"#;
450
451 labels.validate_schema_json_str(schema).unwrap();
452 }
453}