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 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 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 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}