nimbus_fml/schema/
validator.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::FMLError;
6use crate::intermediate_representation::{FeatureDef, FeatureManifest, TypeFinder, TypeRef};
7use crate::{
8    error::Result,
9    intermediate_representation::{EnumDef, ObjectDef},
10};
11use regex::Regex;
12use std::collections::{BTreeMap, HashSet};
13
14const DISALLOWED_PREFS: &[(&str, &str)] = &[
15    (
16        r#"^app\.shield\.optoutstudies\.enabled$"#,
17        "disabling Nimbus causes immediate unenrollment",
18    ),
19    (
20        r#"^datareporting\.healthreport\.uploadEnabled$"#,
21        "disabling telemetry causes immediate unenrollment",
22    ),
23    (
24        r#"^services\.settings\.server$"#,
25        "changing the Remote Settings endpoint will break clients",
26    ),
27    (r#"^nimbus\.debug$"#, "internal Nimbus preference for QA"),
28    (
29        r#"^security\.turn_off_all_security_so_that_viruses_can_take_over_this_computer$"#,
30        "this pref is automation-only and is unsafe to enable outside tests",
31    ),
32];
33
34pub(crate) struct SchemaValidator<'a> {
35    enum_defs: &'a BTreeMap<String, EnumDef>,
36    object_defs: &'a BTreeMap<String, ObjectDef>,
37}
38
39impl<'a> SchemaValidator<'a> {
40    pub(crate) fn new(
41        enums: &'a BTreeMap<String, EnumDef>,
42        objs: &'a BTreeMap<String, ObjectDef>,
43    ) -> Self {
44        Self {
45            enum_defs: enums,
46            object_defs: objs,
47        }
48    }
49
50    fn _get_enum(&self, nm: &str) -> Option<&EnumDef> {
51        self.enum_defs.get(nm)
52    }
53
54    fn get_object(&self, nm: &str) -> Option<&ObjectDef> {
55        self.object_defs.get(nm)
56    }
57
58    pub(crate) fn validate_object_def(&self, object_def: &ObjectDef) -> Result<()> {
59        let obj_nm = &object_def.name;
60        for prop in &object_def.props {
61            let prop_nm = &prop.name;
62
63            // Check the types exist for this property.
64            let path = format!("objects/{obj_nm}/{prop_nm}");
65            self.validate_type_ref(&path, &prop.typ)?;
66        }
67
68        Ok(())
69    }
70
71    pub(crate) fn validate_feature_def(&self, feature_def: &FeatureDef) -> Result<()> {
72        let feat_nm = &feature_def.name;
73        let mut string_aliases: HashSet<_> = Default::default();
74
75        for prop in &feature_def.props {
76            let prop_nm = &prop.name;
77            let prop_t = &prop.typ;
78
79            let path = format!("features/{feat_nm}/{prop_nm}");
80
81            // Check the types exist for this property.
82            self.validate_type_ref(&path, prop_t)?;
83
84            // Check pref is not in the disallowed prefs list.
85            if let Some(pref) = &prop.gecko_pref {
86                for (pref_str, error) in DISALLOWED_PREFS {
87                    let regex = Regex::new(pref_str)?;
88                    if regex.is_match(&pref.pref()) {
89                        return Err(FMLError::ValidationError(
90                            path,
91                            format!(
92                                "Cannot use pref `{}` in experiments, reason: {}",
93                                pref.pref(),
94                                error
95                            ),
96                        ));
97                    }
98                }
99            }
100
101            // Check pref support for this type.
102            if prop.gecko_pref.is_some() && !prop.typ.supports_prefs() {
103                return Err(FMLError::ValidationError(
104                    path,
105                    "Pref keys can only be used with Boolean, String, Int and Text variables"
106                        .to_string(),
107                ));
108            }
109
110            // Check string-alias definition.
111            if let Some(sa) = &prop.string_alias {
112                // Check that the string-alias has only been defined once in this feature.
113                if !string_aliases.insert(sa) {
114                    return Err(FMLError::ValidationError(
115                        path,
116                        format!("The string-alias {sa} should only be declared once per feature"),
117                    ));
118                }
119
120                // Check that the string-alias is actually used in this property type.
121                let types = prop_t.all_types();
122                if !types.contains(sa) {
123                    return Err(FMLError::ValidationError(
124                        path,
125                        format!(
126                            "The string-alias {sa} must be part of the {} type declaration",
127                            prop_nm
128                        ),
129                    ));
130                }
131            }
132        }
133
134        // Now check that that there is a path from this feature to any objects using the
135        // string-aliases defined in this feature.
136        let types = feature_def.all_types();
137        self.validate_string_alias_declarations(
138            &format!("features/{feat_nm}"),
139            feat_nm,
140            &types,
141            &string_aliases,
142        )?;
143
144        Ok(())
145    }
146
147    pub(crate) fn validate_prefs(&self, feature_manifest: &FeatureManifest) -> Result<()> {
148        let prefs = feature_manifest
149            .iter_gecko_prefs()
150            .map(|p| p.pref())
151            .collect::<Vec<String>>();
152        for pref in prefs.clone() {
153            if prefs
154                .iter()
155                .map(|p| if p == &pref { 1 } else { 0 })
156                .sum::<i32>()
157                > 1
158            {
159                let path = format!(r#"prefs/"{}""#, pref);
160                return Err(FMLError::ValidationError(
161                    path,
162                    "Prefs can only be include once per feature manifest".into(),
163                ));
164            }
165        }
166
167        Ok(())
168    }
169
170    fn validate_string_alias_declarations(
171        &self,
172        path: &str,
173        feature: &str,
174        types: &HashSet<TypeRef>,
175        string_aliases: &HashSet<&TypeRef>,
176    ) -> Result<()> {
177        let unaccounted: Vec<_> = types
178            .iter()
179            .filter(|t| matches!(t, TypeRef::StringAlias(_)))
180            .filter(|t| !string_aliases.contains(t))
181            .collect();
182
183        if !unaccounted.is_empty() {
184            let t = unaccounted.first().unwrap();
185            return Err(FMLError::ValidationError(
186                path.to_string(),
187                format!("A string-alias {t} is used by– but has not been defined in– the {feature} feature"),
188            ));
189        }
190        for t in types {
191            if let TypeRef::Object(nm) = t {
192                if let Some(obj) = self.get_object(nm) {
193                    let types = obj.all_types();
194                    self.validate_string_alias_declarations(
195                        &format!("objects/{nm}"),
196                        feature,
197                        &types,
198                        string_aliases,
199                    )?;
200                }
201            }
202        }
203        Ok(())
204    }
205
206    fn validate_type_ref(&self, path: &str, type_ref: &TypeRef) -> Result<()> {
207        match type_ref {
208            TypeRef::Enum(name) => {
209                if !self.enum_defs.contains_key(name) {
210                    return Err(FMLError::ValidationError(
211                        path.to_string(),
212                        format!("Found enum reference with name: {name}, but no definition"),
213                    ));
214                }
215            }
216            TypeRef::Object(name) => {
217                if !self.object_defs.contains_key(name) {
218                    return Err(FMLError::ValidationError(
219                        path.to_string(),
220                        format!("Found object reference with name: {name}, but no definition"),
221                    ));
222                }
223            }
224            TypeRef::EnumMap(key_type, value_type) => match key_type.as_ref() {
225                TypeRef::Enum(_) | TypeRef::String | TypeRef::StringAlias(_) => {
226                    self.validate_type_ref(path, key_type)?;
227                    self.validate_type_ref(path, value_type)?;
228                }
229                _ => {
230                    return Err(FMLError::ValidationError(
231                        path.to_string(),
232                        format!(
233                            "Map key must be a String, string-alias or enum, found: {key_type:?}",
234                        ),
235                    ))
236                }
237            },
238            TypeRef::List(list_type) => self.validate_type_ref(path, list_type)?,
239            TypeRef::StringMap(value_type) => self.validate_type_ref(path, value_type)?,
240            TypeRef::Option(option_type) => {
241                if let TypeRef::Option(_) = option_type.as_ref() {
242                    return Err(FMLError::ValidationError(
243                        path.to_string(),
244                        "Found nested optional types".into(),
245                    ));
246                } else {
247                    self.validate_type_ref(path, option_type)?
248                }
249            }
250            _ => (),
251        };
252        Ok(())
253    }
254}
255
256#[cfg(test)]
257mod manifest_schema {
258    use serde_json::json;
259
260    use super::*;
261    use crate::error::Result;
262    use crate::intermediate_representation::{PrefBranch, PropDef};
263
264    #[test]
265    fn validate_enum_type_ref_doesnt_match_def() -> Result<()> {
266        let enums = Default::default();
267        let objs = Default::default();
268        let validator = SchemaValidator::new(&enums, &objs);
269        let fm = FeatureDef::new(
270            "some_def",
271            "test doc",
272            vec![PropDef::new(
273                "prop name",
274                &TypeRef::Enum("EnumDoesntExist".into()),
275                &json!(null),
276            )],
277            false,
278        );
279        validator.validate_feature_def(&fm).expect_err(
280            "Should fail since EnumDoesntExist isn't a an enum defined in the manifest",
281        );
282        Ok(())
283    }
284
285    #[test]
286    fn validate_obj_type_ref_doesnt_match_def() -> Result<()> {
287        let enums = Default::default();
288        let objs = Default::default();
289        let validator = SchemaValidator::new(&enums, &objs);
290        let fm = FeatureDef::new(
291            "some_def",
292            "test doc",
293            vec![PropDef::new(
294                "prop name",
295                &TypeRef::Object("ObjDoesntExist".into()),
296                &json!(null),
297            )],
298            false,
299        );
300        validator.validate_feature_def(&fm).expect_err(
301            "Should fail since ObjDoesntExist isn't a an Object defined in the manifest",
302        );
303        Ok(())
304    }
305
306    #[test]
307    fn validate_enum_map_with_non_enum_key() -> Result<()> {
308        let enums = Default::default();
309        let objs = Default::default();
310        let validator = SchemaValidator::new(&enums, &objs);
311        let fm = FeatureDef::new(
312            "some_def",
313            "test doc",
314            vec![PropDef::new(
315                "prop_name",
316                &TypeRef::EnumMap(Box::new(TypeRef::Int), Box::new(TypeRef::String)),
317                &json!(null),
318            )],
319            false,
320        );
321        validator
322            .validate_feature_def(&fm)
323            .expect_err("Should fail since the key on an EnumMap must be an Enum");
324        Ok(())
325    }
326
327    #[test]
328    fn validate_list_with_enum_with_no_def() -> Result<()> {
329        let enums = Default::default();
330        let objs = Default::default();
331        let validator = SchemaValidator::new(&enums, &objs);
332        let fm = FeatureDef::new(
333            "some_def",
334            "test doc",
335            vec![PropDef::new(
336                "prop name",
337                &TypeRef::List(Box::new(TypeRef::Enum("EnumDoesntExist".into()))),
338                &json!(null),
339            )],
340            false,
341        );
342        validator
343            .validate_feature_def(&fm)
344            .expect_err("Should fail EnumDoesntExist isn't a an enum defined in the manifest");
345        Ok(())
346    }
347
348    #[test]
349    fn validate_enum_map_with_enum_with_no_def() -> Result<()> {
350        let enums = Default::default();
351        let objs = Default::default();
352        let validator = SchemaValidator::new(&enums, &objs);
353        let fm = FeatureDef::new(
354            "some_def",
355            "test doc",
356            vec![PropDef::new(
357                "prop name",
358                &TypeRef::EnumMap(
359                    Box::new(TypeRef::Enum("EnumDoesntExist".into())),
360                    Box::new(TypeRef::String),
361                ),
362                &json!(null),
363            )],
364            false,
365        );
366        validator.validate_feature_def(&fm).expect_err(
367            "Should fail since EnumDoesntExist isn't a an enum defined in the manifest",
368        );
369        Ok(())
370    }
371
372    #[test]
373    fn validate_enum_map_with_obj_value_no_def() -> Result<()> {
374        let enums = Default::default();
375        let objs = Default::default();
376        let validator = SchemaValidator::new(&enums, &objs);
377        let fm = FeatureDef::new(
378            "some_def",
379            "test doc",
380            vec![PropDef::new(
381                "prop name",
382                &TypeRef::EnumMap(
383                    Box::new(TypeRef::String),
384                    Box::new(TypeRef::Object("ObjDoesntExist".into())),
385                ),
386                &json!(null),
387            )],
388            false,
389        );
390        validator
391            .validate_feature_def(&fm)
392            .expect_err("Should fail since ObjDoesntExist isn't an Object defined in the manifest");
393        Ok(())
394    }
395
396    #[test]
397    fn validate_string_map_with_enum_value_no_def() -> Result<()> {
398        let enums = Default::default();
399        let objs = Default::default();
400        let validator = SchemaValidator::new(&enums, &objs);
401        let fm = FeatureDef::new(
402            "some_def",
403            "test doc",
404            vec![PropDef::new(
405                "prop name",
406                &TypeRef::StringMap(Box::new(TypeRef::Enum("EnumDoesntExist".into()))),
407                &json!(null),
408            )],
409            false,
410        );
411        validator
412            .validate_feature_def(&fm)
413            .expect_err("Should fail since ObjDoesntExist isn't an Object defined in the manifest");
414        Ok(())
415    }
416
417    #[test]
418    fn validate_nested_optionals_fail() -> Result<()> {
419        let enums = Default::default();
420        let objs = Default::default();
421        let validator = SchemaValidator::new(&enums, &objs);
422        let fm = FeatureDef::new(
423            "some_def",
424            "test doc",
425            vec![PropDef::new(
426                "prop name",
427                &TypeRef::Option(Box::new(TypeRef::Option(Box::new(TypeRef::String)))),
428                &json!(null),
429            )],
430            false,
431        );
432        validator
433            .validate_feature_def(&fm)
434            .expect_err("Should fail since we can't have nested optionals");
435        Ok(())
436    }
437
438    #[test]
439    fn validate_disallowed_pref_fails() -> Result<()> {
440        let enums = Default::default();
441        let objs = Default::default();
442        let validator = SchemaValidator::new(&enums, &objs);
443        let fm = FeatureDef::new(
444            "some_def",
445            "test doc",
446            vec![PropDef::new_with_gecko_pref(
447                "prop name",
448                &TypeRef::String,
449                &json!(null),
450                "app.shield.optoutstudies.enabled",
451                PrefBranch::User,
452            )],
453            false,
454        );
455        validator
456            .validate_feature_def(&fm)
457            .expect_err("Should fail since we can't use that pref for experimentation");
458        Ok(())
459    }
460}
461
462#[cfg(test)]
463mod string_aliases {
464    use serde_json::json;
465
466    use crate::intermediate_representation::PropDef;
467
468    use super::*;
469
470    fn with_objects(objects: &[ObjectDef]) -> BTreeMap<String, ObjectDef> {
471        let mut obj_defs: BTreeMap<_, _> = Default::default();
472        for o in objects {
473            obj_defs.insert(o.name(), o.clone());
474        }
475        obj_defs
476    }
477
478    fn with_feature(props: &[PropDef]) -> FeatureDef {
479        FeatureDef::new("test-feature", "", props.into(), false)
480    }
481
482    #[test]
483    fn test_validate_feature_schema() -> Result<()> {
484        let name = TypeRef::StringAlias("PersonName".to_string());
485        let all_names = {
486            let t = TypeRef::List(Box::new(name.clone()));
487            let v = json!(["Alice", "Bonnie", "Charlie", "Denise", "Elise", "Frankie"]);
488            PropDef::with_string_alias("all-names", &t, &v, &name)
489        };
490
491        let all_names2 = {
492            let t = TypeRef::List(Box::new(name.clone()));
493            let v = json!(["Alice", "Bonnie"]);
494            PropDef::with_string_alias("all-names-duplicate", &t, &v, &name)
495        };
496
497        let enums = Default::default();
498        let objects = Default::default();
499        let validator = SchemaValidator::new(&enums, &objects);
500
501        // -> Verify that only one property per feature can define the same string-alias.
502        let fm = with_feature(&[all_names.clone(), all_names2.clone()]);
503        assert!(validator.validate_feature_def(&fm).is_err());
504
505        let newest_member = {
506            let t = &name;
507            let v = json!("Alice"); // it doesn't matter for this test what the value is.
508            PropDef::new("newest-member", t, &v)
509        };
510
511        // -> Verify that a property in a feature can validate against the a string-alias
512        // -> in the same feature.
513        // { all-names: ["Alice"], newest-member: "Alice" }
514        let fm = with_feature(&[all_names.clone(), newest_member.clone()]);
515        validator.validate_feature_def(&fm)?;
516
517        // { newest-member: "Alice" }
518        // We have a reference to a team mate, but no definitions.
519        // Should error out.
520        let fm = with_feature(&[newest_member.clone()]);
521        assert!(validator.validate_feature_def(&fm).is_err());
522
523        // -> Validate a property in a nested object can validate against a string-alias
524        // -> in a feature that uses the object.
525        let team_def = ObjectDef::new("Team", &[newest_member.clone()]);
526        let team = {
527            let t = TypeRef::Object("Team".to_string());
528            let v = json!({ "newest-member": "Alice" });
529
530            PropDef::new("team", &t, &v)
531        };
532
533        // { all-names: ["Alice"], team: { newest-member: "Alice" } }
534        let fm = with_feature(&[all_names.clone(), team.clone()]);
535        let objs = with_objects(&[team_def.clone()]);
536        let validator = SchemaValidator::new(&enums, &objs);
537        validator.validate_feature_def(&fm)?;
538
539        // { team: { newest-member: "Alice" } }
540        let fm = with_feature(&[team.clone()]);
541        let objs = with_objects(&[team_def.clone()]);
542        let validator = SchemaValidator::new(&enums, &objs);
543        assert!(validator.validate_feature_def(&fm).is_err());
544
545        // -> Validate a property in a deeply nested object can validate against a string-alias
546        // -> in a feature that uses the object.
547
548        let match_def = ObjectDef::new("Match", &[team.clone()]);
549        let match_ = {
550            let t = TypeRef::Object("Match".to_string());
551            let v = json!({ "team": { "newest-member": "Alice" }});
552
553            PropDef::new("match", &t, &v)
554        };
555
556        // { all-names: ["Alice"], match: { team: { newest-member: "Alice" }} }
557        let fm = with_feature(&[all_names.clone(), match_.clone()]);
558        let objs = with_objects(&[team_def.clone(), match_def.clone()]);
559        let validator = SchemaValidator::new(&enums, &objs);
560        validator.validate_feature_def(&fm)?;
561
562        // { match: {team: { newest-member: "Alice" }} }
563        let fm = with_feature(&[match_.clone()]);
564        let validator = SchemaValidator::new(&enums, &objs);
565        assert!(validator.validate_feature_def(&fm).is_err());
566
567        Ok(())
568    }
569}