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