1use crate::graph::{AuthorityGraph, EdgeKind, NodeKind};
2
3#[derive(Debug)]
5pub struct MapRow {
6 pub step_name: String,
7 pub trust_zone: String,
8 pub access: Vec<bool>,
10}
11
12#[derive(Debug)]
14pub struct AuthorityMap {
15 pub authorities: Vec<String>,
17 pub rows: Vec<MapRow>,
19}
20
21pub fn authority_map(graph: &AuthorityGraph) -> AuthorityMap {
23 let authorities: Vec<_> = graph
25 .authority_sources()
26 .map(|n| (n.id, n.name.clone()))
27 .collect();
28
29 let authority_names: Vec<String> = authorities.iter().map(|(_, name)| name.clone()).collect();
30
31 let mut rows = Vec::new();
33 for step in graph.nodes_of_kind(NodeKind::Step) {
34 let mut access = vec![false; authorities.len()];
35
36 for edge in graph.edges_from(step.id) {
37 if edge.kind != EdgeKind::HasAccessTo {
38 continue;
39 }
40 if let Some(idx) = authorities.iter().position(|(id, _)| *id == edge.to) {
42 access[idx] = true;
43 }
44 }
45
46 rows.push(MapRow {
47 step_name: step.name.clone(),
48 trust_zone: format!("{:?}", step.trust_zone),
49 access,
50 });
51 }
52
53 AuthorityMap {
54 authorities: authority_names,
55 rows,
56 }
57}
58
59pub fn render_map(map: &AuthorityMap) -> String {
61 if map.rows.is_empty() && map.authorities.is_empty() {
62 return "No steps or authority sources found.\n".to_string();
63 }
64
65 let step_width = map
67 .rows
68 .iter()
69 .map(|r| r.step_name.len())
70 .max()
71 .unwrap_or(4)
72 .max(4);
73
74 let zone_width = map
75 .rows
76 .iter()
77 .map(|r| r.trust_zone.len())
78 .max()
79 .unwrap_or(4)
80 .max(4);
81
82 let auth_widths: Vec<usize> = map.authorities.iter().map(|a| a.len().max(3)).collect();
83
84 let mut out = String::new();
85
86 out.push_str(&format!(
88 "{:<step_w$} {:<zone_w$}",
89 "Step",
90 "Zone",
91 step_w = step_width,
92 zone_w = zone_width
93 ));
94 for (i, auth) in map.authorities.iter().enumerate() {
95 out.push_str(&format!(" {:^w$}", auth, w = auth_widths[i]));
96 }
97 out.push('\n');
98
99 out.push_str(&"-".repeat(step_width));
101 out.push_str(" ");
102 out.push_str(&"-".repeat(zone_width));
103 for w in &auth_widths {
104 out.push_str(" ");
105 out.push_str(&"-".repeat(*w));
106 }
107 out.push('\n');
108
109 for row in &map.rows {
111 out.push_str(&format!(
112 "{:<step_w$} {:<zone_w$}",
113 row.step_name,
114 row.trust_zone,
115 step_w = step_width,
116 zone_w = zone_width
117 ));
118 for (i, &has) in row.access.iter().enumerate() {
119 let marker = if has { "X" } else { "." };
120 out.push_str(&format!(" {:^w$}", marker, w = auth_widths[i]));
121 }
122 out.push('\n');
123 }
124
125 out
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::graph::*;
132
133 fn source(file: &str) -> PipelineSource {
134 PipelineSource {
135 file: file.into(),
136 repo: None,
137 git_ref: None,
138 }
139 }
140
141 #[test]
142 fn map_shows_step_access() {
143 let mut g = AuthorityGraph::new(source("ci.yml"));
144 let secret = g.add_node(NodeKind::Secret, "API_KEY", TrustZone::FirstParty);
145 let token = g.add_node(NodeKind::Identity, "GITHUB_TOKEN", TrustZone::FirstParty);
146 let build = g.add_node(NodeKind::Step, "build", TrustZone::FirstParty);
147 let deploy = g.add_node(NodeKind::Step, "deploy", TrustZone::ThirdParty);
148
149 g.add_edge(build, secret, EdgeKind::HasAccessTo);
150 g.add_edge(build, token, EdgeKind::HasAccessTo);
151 g.add_edge(deploy, token, EdgeKind::HasAccessTo);
152
153 let map = authority_map(&g);
154 assert_eq!(map.authorities.len(), 2);
155 assert_eq!(map.rows.len(), 2);
156
157 let build_row = &map.rows[0];
159 assert!(build_row.access[0]); assert!(build_row.access[1]); let deploy_row = &map.rows[1];
164 assert!(!deploy_row.access[0]); assert!(deploy_row.access[1]); }
167
168 #[test]
169 fn map_renders_table() {
170 let mut g = AuthorityGraph::new(source("ci.yml"));
171 let secret = g.add_node(NodeKind::Secret, "KEY", TrustZone::FirstParty);
172 let step = g.add_node(NodeKind::Step, "build", TrustZone::FirstParty);
173 g.add_edge(step, secret, EdgeKind::HasAccessTo);
174
175 let map = authority_map(&g);
176 let table = render_map(&map);
177 assert!(table.contains("build"));
178 assert!(table.contains("KEY"));
179 assert!(table.contains("X"));
180 }
181}