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