1pub const DENIED_TYPES: &[(&str, &str)] = &[
11 ("ignition", "security-properties"),
12 ("ignition", "security-levels"),
13 ("ignition", "api-token"),
14 ("ignition", "user-source"),
15];
16
17pub const GATEWAY_OWNED_FILES: &[&str] = &["config-mode.json"];
21
22pub fn is_denied(module: &str, ty: &str) -> bool {
24 DENIED_TYPES.iter().any(|(m, t)| *m == module && *t == ty)
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum UuidRule {
32 Singleton,
35 Named,
38 NamedNoUuid,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct TypeRule {
49 pub module: &'static str,
51 pub ty: &'static str,
53 pub uuid: UuidRule,
55 pub scope: &'static str,
57}
58
59const fn row(
60 module: &'static str,
61 ty: &'static str,
62 uuid: UuidRule,
63 scope: &'static str,
64) -> TypeRule {
65 TypeRule {
66 module,
67 ty,
68 uuid,
69 scope,
70 }
71}
72
73pub const TYPE_RULES: &[TypeRule] = &[
84 row(
86 "com.cirruslink.mqtt.distributor.gateway",
87 "general",
88 UuidRule::Singleton,
89 "A",
90 ),
91 row(
93 "com.cirruslink.mqtt.distributor.gateway",
94 "user",
95 UuidRule::Named,
96 "A",
97 ),
98 row(
100 "com.cirruslink.mqtt.engine.gateway",
101 "default-namespace",
102 UuidRule::Named,
103 "A",
104 ),
105 row(
107 "com.cirruslink.mqtt.engine.gateway",
108 "general",
109 UuidRule::Singleton,
110 "A",
111 ),
112 row(
114 "com.cirruslink.mqtt.engine.gateway",
115 "namespace-file",
116 UuidRule::Singleton,
117 "A",
118 ),
119 row(
121 "com.cirruslink.mqtt.engine.gateway",
122 "namespace-server-set",
123 UuidRule::Named,
124 "A",
125 ),
126 row(
128 "com.cirruslink.mqtt.engine.gateway",
129 "server",
130 UuidRule::Named,
131 "A",
132 ),
133 row(
135 "com.cirruslink.mqtt.engine.gateway",
136 "server-set",
137 UuidRule::Named,
138 "A",
139 ),
140 row(
142 "com.cirruslink.mqtt.transmission.gateway",
143 "general",
144 UuidRule::Singleton,
145 "A",
146 ),
147 row(
149 "com.cirruslink.mqtt.transmission.gateway",
150 "history-store",
151 UuidRule::Named,
152 "A",
153 ),
154 row(
156 "com.cirruslink.mqtt.transmission.gateway",
157 "server",
158 UuidRule::Named,
159 "A",
160 ),
161 row(
163 "com.cirruslink.mqtt.transmission.gateway",
164 "server-set",
165 UuidRule::Named,
166 "A",
167 ),
168 row(
170 "com.cirruslink.mqtt.transmission.gateway",
171 "transmitter",
172 UuidRule::Named,
173 "A",
174 ),
175 row(
177 "com.inductiveautomation.alarm-notification",
178 "alarm-notification-profile",
179 UuidRule::Named,
180 "A",
181 ),
182 row(
184 "com.inductiveautomation.eam",
185 "agent-group",
186 UuidRule::Named,
187 "A",
188 ),
189 row(
191 "com.inductiveautomation.eam",
192 "event-thresholds",
193 UuidRule::Singleton,
194 "A",
195 ),
196 row(
198 "com.inductiveautomation.eam",
199 "module-certificates",
200 UuidRule::Named,
201 "A",
202 ),
203 row(
205 "com.inductiveautomation.eam",
206 "module-eulas",
207 UuidRule::Named,
208 "A",
209 ),
210 row(
212 "com.inductiveautomation.eam",
213 "module-settings",
214 UuidRule::Singleton,
215 "A",
216 ),
217 row(
219 "com.inductiveautomation.historian",
220 "historian-provider",
221 UuidRule::Named,
222 "A",
223 ),
224 row(
226 "com.inductiveautomation.opcua",
227 "access-control",
228 UuidRule::Singleton,
229 "A",
230 ),
231 row(
233 "com.inductiveautomation.opcua",
234 "device",
235 UuidRule::Named,
236 "A",
237 ),
238 row(
240 "com.inductiveautomation.opcua",
241 "one-time",
242 UuidRule::Singleton,
243 "A",
244 ),
245 row(
247 "com.inductiveautomation.opcua",
248 "server-config",
249 UuidRule::Singleton,
250 "A",
251 ),
252 row(
254 "com.inductiveautomation.perspective",
255 "themes",
256 UuidRule::NamedNoUuid,
257 "G",
258 ),
259 row(
261 "com.inductiveautomation.sfc",
262 "chart-settings",
263 UuidRule::Singleton,
264 "A",
265 ),
266 row("ignition", "alarm-journal", UuidRule::Named, "A"),
268 row("ignition", "audit-profile", UuidRule::Named, "A"),
270 row("ignition", "cobranding", UuidRule::Singleton, "A"),
272 row("ignition", "database-connection", UuidRule::Named, "A"),
274 row("ignition", "database-driver", UuidRule::Named, "A"),
276 row("ignition", "database-translator", UuidRule::Named, "A"),
278 row("ignition", "edge-sync-settings", UuidRule::Singleton, "A"),
280 row(
282 "ignition",
283 "edge-system-properties",
284 UuidRule::Singleton,
285 "A",
286 ),
287 row("ignition", "email-profile", UuidRule::Named, "A"),
289 row(
291 "ignition",
292 "gateway-network-proxy-rules",
293 UuidRule::Singleton,
294 "A",
295 ),
296 row(
298 "ignition",
299 "gateway-network-queue-settings",
300 UuidRule::Singleton,
301 "A",
302 ),
303 row(
305 "ignition",
306 "gateway-network-settings",
307 UuidRule::Singleton,
308 "A",
309 ),
310 row(
312 "ignition",
313 "general-alarm-settings",
314 UuidRule::Singleton,
315 "A",
316 ),
317 row("ignition", "holiday", UuidRule::Named, "A"),
319 row("ignition", "identity-provider", UuidRule::Named, "A"),
321 row("ignition", "oauth2-client", UuidRule::Named, "A"),
323 row("ignition", "opc-connection", UuidRule::Named, "A"),
325 row("ignition", "quickstart", UuidRule::Singleton, "A"),
327 row("ignition", "roster-config", UuidRule::Named, "A"),
329 row("ignition", "schedule", UuidRule::Named, "A"),
331 row("ignition", "security-zone", UuidRule::Named, "A"),
333 row("ignition", "service-connector", UuidRule::Named, "A"),
335 row(
337 "ignition",
338 "store-and-forward-engine",
339 UuidRule::NamedNoUuid,
340 "G",
341 ),
342 row("ignition", "system-properties", UuidRule::Singleton, "A"),
344];
345
346pub fn type_rule(module: &str, ty: &str) -> Option<&'static TypeRule> {
348 TYPE_RULES
349 .binary_search_by(|r| {
350 r.module
351 .as_bytes()
352 .cmp(module.as_bytes())
353 .then_with(|| r.ty.as_bytes().cmp(ty.as_bytes()))
354 })
355 .ok()
356 .map(|i| &TYPE_RULES[i])
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub struct SecretFields {
365 pub module: &'static str,
367 pub ty: &'static str,
369 pub paths: &'static [&'static str],
371}
372
373pub const SECRET_FIELDS: &[SecretFields] = &[
379 SecretFields {
380 module: "com.inductiveautomation.alarm-notification",
381 ty: "alarm-notification-profile",
382 paths: &[
383 "settings.settings.password",
384 "settings.twoWayPop3.pop3Password",
385 "settings.voipGateway.password",
386 ],
387 },
388 SecretFields {
389 module: "com.inductiveautomation.opcua",
390 ty: "device",
391 paths: &["settings.advanced.password"],
392 },
393 SecretFields {
394 module: "ignition",
395 ty: "database-connection",
396 paths: &["password"],
397 },
398 SecretFields {
399 module: "ignition",
400 ty: "email-profile",
401 paths: &["settings.settings.password"],
402 },
403 SecretFields {
404 module: "ignition",
405 ty: "identity-provider",
406 paths: &["settings.clientSecret"],
407 },
408 SecretFields {
409 module: "ignition",
410 ty: "oauth2-client",
411 paths: &["clientSecret"],
412 },
413 SecretFields {
414 module: "ignition",
415 ty: "opc-connection",
416 paths: &[
417 "settings.authentication.password",
418 "settings.authentication.privateKeyPem",
419 "settings.security.keyStoreAliasPassword",
420 ],
421 },
422 SecretFields {
423 module: "ignition",
424 ty: "service-connector",
425 paths: &[
426 "settings.password",
427 "settings.ssl.keyPassword",
428 "settings.ssl.keystore.key",
429 "settings.ssl.keystore.password",
430 "settings.ssl.truststore.password",
431 ],
432 },
433 SecretFields {
434 module: "ignition",
435 ty: "system-properties",
436 paths: &["errorReportPassword"],
437 },
438 SecretFields {
439 module: "ignition",
440 ty: "user-source",
441 paths: &[
442 "settings.connectionPassword",
443 "settings.ldap.connectionPassword",
444 ],
445 },
446];
447
448pub fn secret_paths(module: &str, ty: &str) -> &'static [&'static str] {
451 SECRET_FIELDS
452 .iter()
453 .find(|s| s.module == module && s.ty == ty)
454 .map_or(&[], |s| s.paths)
455}
456
457pub const OPENAPI_TYPES: &[(&str, &str)] = &[
463 (
464 "com.inductiveautomation.alarm-notification",
465 "alarm-notification-profile",
466 ),
467 ("com.inductiveautomation.eam", "agent-group"),
468 ("com.inductiveautomation.eam", "agent-management"),
469 ("com.inductiveautomation.eam", "eam-tasks"),
470 ("com.inductiveautomation.eam", "event-thresholds"),
471 ("com.inductiveautomation.eam", "hw-license-management"),
472 ("com.inductiveautomation.eam", "leased-license-management"),
473 ("com.inductiveautomation.eam", "module-certificates"),
474 ("com.inductiveautomation.eam", "module-eulas"),
475 ("com.inductiveautomation.eam", "module-settings"),
476 ("com.inductiveautomation.eam", "remote-upgrade"),
477 ("com.inductiveautomation.historian", "historian-provider"),
478 ("com.inductiveautomation.opcua", "access-control"),
479 ("com.inductiveautomation.opcua", "device"),
480 ("com.inductiveautomation.opcua", "server-config"),
481 (
482 "com.inductiveautomation.opcua.drivers.bacnet",
483 "BacnetIpLocalDeviceConfig",
484 ),
485 ("com.inductiveautomation.perspective", "fonts"),
486 ("com.inductiveautomation.perspective", "icons"),
487 ("com.inductiveautomation.perspective", "themes"),
488 ("com.inductiveautomation.sfc", "chart-settings"),
489 (
490 "com.inductiveautomation.sip-notification",
491 "script-settings",
492 ),
493 ("ignition", "alarm-journal"),
494 ("ignition", "api-token"),
495 ("ignition", "audit-profile"),
496 ("ignition", "cobranding"),
497 ("ignition", "database-connection"),
498 ("ignition", "database-driver"),
499 ("ignition", "database-translator"),
500 ("ignition", "edge-system-properties"),
501 ("ignition", "email-profile"),
502 ("ignition", "gateway-network-incoming"),
503 ("ignition", "gateway-network-outgoing"),
504 ("ignition", "gateway-network-proxy-rules"),
505 ("ignition", "gateway-network-queue-settings"),
506 ("ignition", "gateway-network-settings"),
507 ("ignition", "general-alarm-settings"),
508 ("ignition", "holiday"),
509 ("ignition", "identity-provider"),
510 ("ignition", "keyboard_layout"),
511 ("ignition", "local-system-properties"),
512 ("ignition", "metrics-dashboard"),
513 ("ignition", "oauth2-client"),
514 ("ignition", "opc-connection"),
515 ("ignition", "quickstart"),
516 ("ignition", "roster-config"),
517 ("ignition", "schedule"),
518 ("ignition", "secret-provider"),
519 ("ignition", "security-levels"),
520 ("ignition", "security-properties"),
521 ("ignition", "security-zone"),
522 ("ignition", "service-connector"),
523 ("ignition", "store-and-forward-engine"),
524 ("ignition", "system-properties"),
525 ("ignition", "tag-provider"),
526 ("ignition", "translations"),
527 ("ignition", "user-source"),
528];
529
530pub fn openapi_describes(module: &str, ty: &str) -> bool {
533 OPENAPI_TYPES.binary_search(&(module, ty)).is_ok()
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 const OPENAPI: &str = include_str!(concat!(
541 env!("CARGO_MANIFEST_DIR"),
542 "/tests/fixtures/deploy/openapi-8.3.6-resource-types.json"
543 ));
544
545 #[test]
546 fn deny_list_is_exact() {
547 assert!(is_denied("ignition", "security-properties"));
548 assert!(is_denied("ignition", "user-source"));
549 assert!(!is_denied("ignition", "database-translator"));
550 assert!(!is_denied("other", "api-token"));
551 }
552
553 #[test]
554 fn type_rules_are_sorted_unique_and_exclude_denied() {
555 for pair in TYPE_RULES.windows(2) {
556 let a = (pair[0].module.as_bytes(), pair[0].ty.as_bytes());
557 let b = (pair[1].module.as_bytes(), pair[1].ty.as_bytes());
558 assert!(
559 a < b,
560 "not strictly sorted: {}/{} then {}/{}",
561 pair[0].module,
562 pair[0].ty,
563 pair[1].module,
564 pair[1].ty
565 );
566 }
567 for r in TYPE_RULES {
568 assert!(
569 !is_denied(r.module, r.ty),
570 "{}/{} is denied",
571 r.module,
572 r.ty
573 );
574 assert!(
575 matches!(r.scope, "A" | "G"),
576 "{}/{} scope {}",
577 r.module,
578 r.ty,
579 r.scope
580 );
581 assert_eq!(type_rule(r.module, r.ty), Some(r));
582 if r.uuid == UuidRule::NamedNoUuid {
583 assert_eq!(r.scope, "G", "{}/{}", r.module, r.ty);
584 }
585 }
586 assert_eq!(type_rule("ignition", "no-such-type"), None);
587 assert_eq!(type_rule("ignition", "security-levels"), None);
588 let no_uuid: Vec<_> = TYPE_RULES
590 .iter()
591 .filter(|r| r.uuid == UuidRule::NamedNoUuid)
592 .map(|r| (r.module, r.ty))
593 .collect();
594 assert_eq!(
595 no_uuid,
596 [
597 ("com.inductiveautomation.perspective", "themes"),
598 ("ignition", "store-and-forward-engine")
599 ]
600 );
601 }
602
603 #[test]
604 fn type_rules_agree_with_openapi_extract() {
605 let doc: serde_json::Value = serde_json::from_str(OPENAPI).expect("extract parses");
606 let types = doc["types"].as_array().expect("types array");
607 assert_eq!(types.len(), 56);
609 assert_eq!(types.iter().filter(|t| t["named"] == true).count(), 39);
610 let mut compared = 0;
611 for t in types {
612 let (module, ty) = (
613 t["module"].as_str().expect("module"),
614 t["type"].as_str().expect("type"),
615 );
616 let Some(rule) = type_rule(module, ty) else {
617 continue;
618 };
619 compared += 1;
620 let named = t["named"].as_bool().expect("named flag");
621 assert_eq!(
622 named,
623 rule.uuid != UuidRule::Singleton,
624 "{module}/{ty}: extract says named={named}, rule says {:?}",
625 rule.uuid
626 );
627 }
628 assert!(compared > 25, "only {compared} rows were comparable");
629 }
630
631 #[test]
632 fn secret_fields_agree_with_openapi_extract() {
633 let doc: serde_json::Value = serde_json::from_str(OPENAPI).expect("extract parses");
634 let fields = doc["secret_fields"].as_array().expect("secret_fields");
635 assert_eq!(fields.len(), SECRET_FIELDS.len());
636 assert_eq!(SECRET_FIELDS.len(), 10);
637 for (row, want) in SECRET_FIELDS.iter().zip(fields) {
638 assert_eq!(row.module, want["module"].as_str().expect("module"));
639 assert_eq!(row.ty, want["type"].as_str().expect("type"));
640 let paths: Vec<&str> = want["paths"]
641 .as_array()
642 .expect("paths")
643 .iter()
644 .map(|p| p.as_str().expect("path"))
645 .collect();
646 assert_eq!(row.paths, paths.as_slice(), "{}/{}", row.module, row.ty);
647 }
648 for pair in SECRET_FIELDS.windows(2) {
649 assert!(
650 (pair[0].module.as_bytes(), pair[0].ty.as_bytes())
651 < (pair[1].module.as_bytes(), pair[1].ty.as_bytes())
652 );
653 }
654 assert_eq!(
655 secret_paths("ignition", "email-profile"),
656 ["settings.settings.password"]
657 );
658 assert!(secret_paths("ignition", "schedule").is_empty());
659 assert!(secret_paths("no.such", "type").is_empty());
660 for row in SECRET_FIELDS {
662 assert!(openapi_describes(row.module, row.ty));
663 }
664 }
665
666 #[test]
667 fn openapi_types_equal_extract() {
668 let doc: serde_json::Value = serde_json::from_str(OPENAPI).expect("extract parses");
669 let mut want: Vec<(String, String)> = doc["types"]
670 .as_array()
671 .expect("types")
672 .iter()
673 .map(|t| {
674 (
675 t["module"].as_str().expect("module").to_string(),
676 t["type"].as_str().expect("type").to_string(),
677 )
678 })
679 .collect();
680 want.sort_by(|a, b| {
681 (a.0.as_bytes(), a.1.as_bytes()).cmp(&(b.0.as_bytes(), b.1.as_bytes()))
682 });
683 let got: Vec<(String, String)> = OPENAPI_TYPES
684 .iter()
685 .map(|(m, t)| ((*m).to_string(), (*t).to_string()))
686 .collect();
687 assert_eq!(got, want);
688 assert_eq!(OPENAPI_TYPES.len(), 56);
689 assert!(openapi_describes("ignition", "database-connection"));
690 assert!(!openapi_describes(
691 "com.cirruslink.mqtt.engine.gateway",
692 "server"
693 ));
694 }
695}