kanidm_cli/
graph.rs
1use crate::common::OpType;
2use crate::{handle_client_error, GraphCommonOpt, GraphType, ObjectType, OutputMode};
3use kanidm_proto::internal::Filter::{Eq, Or};
4
5impl GraphCommonOpt {
6 pub fn debug(&self) -> bool {
7 self.copt.debug
8 }
9
10 pub async fn exec(&self) {
11 let gopt: &GraphCommonOpt = self;
12 let copt = &gopt.copt;
13 let client = copt.to_client(OpType::Read).await;
14 let graph_type = &gopt.graph_type;
15 let filters = &gopt.filter;
16
17 let filter = Or(vec![
18 Eq("class".to_string(), "person".to_string()),
19 Eq("class".to_string(), "service_account".to_string()),
20 Eq("class".to_string(), "group".to_string()),
21 ]);
22
23 let result = client.search(filter).await;
24
25 let entries = match result {
26 Ok(entries) => entries,
27 Err(e) => {
28 handle_client_error(e, copt.output_mode);
29 return;
30 }
31 };
32
33 match copt.output_mode {
34 OutputMode::Json => {
35 let r_attrs: Vec<_> = entries.iter().map(|entry| &entry.attrs).collect();
36 println!(
37 "{}",
38 serde_json::to_string(&r_attrs).expect("Failed to serialise json")
39 );
40 }
41 OutputMode::Text => {
42 eprintln!("Showing graph for type: {graph_type:?}, filters: {filters:?}\n");
43 let typed_entries = entries
44 .iter()
45 .filter_map(|entry| {
46 let classes = entry.attrs.get("class")?;
47 let uuid = entry.attrs.get("uuid")?.first()?;
48
49 let obj_type = if classes.contains(&"group".to_string()) {
51 if uuid.starts_with("00000000-0000-0000-0000-") {
52 ObjectType::BuiltinGroup
53 } else {
54 ObjectType::Group
55 }
56 } else if classes.contains(&"account".to_string()) {
57 if classes.contains(&"person".to_string()) {
58 ObjectType::Person
59 } else if classes.contains(&"service-account".to_string()) {
60 ObjectType::ServiceAccount
61 } else {
62 return None;
63 }
64 } else {
65 return None;
66 };
67
68 if !filters.contains(&obj_type) && !filters.is_empty() {
70 return None;
71 }
72
73 let spn = entry.attrs.get("spn")?.first()?;
74 Some((spn.clone(), uuid.clone(), obj_type))
75 })
76 .collect::<Vec<(String, String, ObjectType)>>();
77
78 let members_of = entries
80 .into_iter()
81 .filter_map(|entry| {
82 let spn = entry.attrs.get("spn")?.first()?.clone();
83 let uuid = entry.attrs.get("uuid")?.first()?.clone();
84 let keep = typed_entries
85 .iter()
86 .any(|(_, filtered_uuid, _)| &uuid == filtered_uuid);
87 if keep {
88 Some((spn, entry.attrs.get("member")?.clone()))
89 } else {
90 None
91 }
92 })
93 .collect::<Vec<_>>();
94
95 match graph_type {
96 GraphType::Graphviz => Self::print_graphviz_graph(&typed_entries, &members_of),
97 GraphType::Mermaid => Self::print_mermaid_graph(typed_entries, members_of),
98 GraphType::MermaidElk => {
99 println!(r#"%%{{init: {{"flowchart": {{"defaultRenderer": "elk"}}}} }}%%"#);
100 Self::print_mermaid_graph(typed_entries, members_of);
101 }
102 }
103 }
104 }
105 }
106
107 fn print_graphviz_graph(
108 typed_entries: &Vec<(String, String, ObjectType)>,
109 members_of: &Vec<(String, Vec<String>)>,
110 ) {
111 println!("digraph {{");
112 println!(r#" rankdir="RL""#);
113
114 for (spn, members) in members_of {
115 members
116 .iter()
117 .filter(|member| typed_entries.iter().any(|(spn, _, _)| spn == *member))
118 .for_each(|member| {
119 println!(r#" "{spn}" -> "{member}""#);
120 });
121 }
122
123 for (spn, _, obj_type) in typed_entries {
124 let (color, shape) = match obj_type {
125 ObjectType::Group => ("#b86367", "box"),
126 ObjectType::BuiltinGroup => ("#8bc1d6", "component"),
127 ObjectType::ServiceAccount => ("#77c98d", "parallelogram"),
128 ObjectType::Person => ("#af8bd6", "ellipse"),
129 };
130
131 println!(r#" "{spn}" [color = "{color}", shape = {shape}]"#);
132 }
133 println!("}}");
134 }
135
136 fn print_mermaid_graph(
137 typed_entries: Vec<(String, String, ObjectType)>,
138 members_of: Vec<(String, Vec<String>)>,
139 ) {
140 println!("graph RL");
141 for (spn, members) in members_of {
142 members
143 .iter()
144 .filter(|member| typed_entries.iter().any(|(spn, _, _)| spn == *member))
145 .for_each(|member| {
146 let at_less_name = Self::mermaid_id_from_spn(&spn);
147 let at_less_member = Self::mermaid_id_from_spn(member);
148 println!(" {at_less_name}[\"{spn}\"] --> {at_less_member}[\"{member}\"]")
149 });
150 }
151 println!(
152 " classDef groupClass fill:#f9f,stroke:#333,stroke-width:4px,stroke-dasharray: 5 5"
153 );
154 println!(" classDef builtInGroupClass fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5");
155 println!(" classDef serviceAccountClass fill:#f9f,stroke:#333,stroke-width:4px");
156 println!(" classDef personClass fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff");
157
158 for (spn, _, obj_type) in typed_entries {
159 let class = match obj_type {
160 ObjectType::Group => "groupClass",
161 ObjectType::BuiltinGroup => "builtInGroupClass",
162 ObjectType::ServiceAccount => "serviceAccountClass",
163 ObjectType::Person => "personClass",
164 };
165 let at_less_name = Self::mermaid_id_from_spn(&spn);
166 println!(" {at_less_name}[\"{spn}\"]");
167 println!(" class {at_less_name} {class}");
168 }
169 }
170
171 fn mermaid_id_from_spn(spn: &str) -> String {
172 spn.replace('@', "_")
173 }
174}