kanidm_cli/
graph.rs

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                        // Logic to decide the type of each entry
47                        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                        // Filter out the things we want to keep, if the filter is empty we assume we want all.
66                        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                // Vec<obj, uuid, obj's members>
76                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}