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