kanidm_cli/
graph.rs

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