1use serde::{Deserialize, Serialize};
2use crate::models::k8s::K8sMetadata;
3use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct PolicyRule {
12 #[serde(default, rename = "apiGroups")]
13 pub api_groups: Vec<String>,
14 #[serde(default)]
15 pub resources: Vec<String>,
16 pub verbs: Vec<String>,
17 #[serde(default, rename = "resourceNames")]
18 pub resource_names: Vec<String>,
19 #[serde(default, rename = "nonResourceURLs")]
20 pub non_resource_urls: Vec<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct K8sRole {
26 #[serde(rename = "apiVersion")]
27 pub api_version: String,
28 pub kind: String,
29 pub metadata: K8sMetadata,
30 pub rules: Vec<PolicyRule>,
31}
32
33impl ConfigValidator for K8sRole {
34 fn yaml_type(&self) -> YamlType {
35 if self.kind == "ClusterRole" { YamlType::K8sClusterRole } else { YamlType::K8sRole }
36 }
37
38 fn validate_structure(&self) -> Vec<Diagnostic> {
39 let mut diags = Vec::new();
40 for (i, rule) in self.rules.iter().enumerate() {
41 if rule.verbs.is_empty() {
42 diags.push(Diagnostic {
43 severity: Severity::Error,
44 message: format!("Rule[{}]: verbs cannot be empty", i),
45 path: Some(format!("rules > {}", i)),
46 });
47 }
48 }
49 diags
50 }
51
52 fn validate_semantics(&self) -> Vec<Diagnostic> {
53 let mut diags = Vec::new();
54 for (i, rule) in self.rules.iter().enumerate() {
55 if rule.resources.contains(&"*".to_string()) || rule.verbs.contains(&"*".to_string()) {
56 diags.push(Diagnostic {
57 severity: Severity::Warning,
58 message: format!("Rule[{}]: wildcard '*' grants broad access — follow least-privilege principle", i),
59 path: Some(format!("rules > {}", i)),
60 });
61 }
62 let dangerous_verbs = ["delete", "deletecollection", "escalate", "impersonate"];
63 for verb in &rule.verbs {
64 if dangerous_verbs.contains(&verb.as_str()) {
65 diags.push(Diagnostic {
66 severity: Severity::Warning,
67 message: format!("Rule[{}]: verb '{}' is potentially dangerous", i, verb),
68 path: Some(format!("rules > {} > verbs", i)),
69 });
70 }
71 }
72 }
73 diags
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct RoleRef {
84 #[serde(rename = "apiGroup")]
85 pub api_group: String,
86 pub kind: String,
87 pub name: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct Subject {
93 pub kind: String,
94 pub name: String,
95 #[serde(default)]
96 pub namespace: Option<String>,
97 #[serde(default, rename = "apiGroup")]
98 pub api_group: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct K8sRoleBinding {
104 #[serde(rename = "apiVersion")]
105 pub api_version: String,
106 pub kind: String,
107 pub metadata: K8sMetadata,
108 #[serde(rename = "roleRef")]
109 pub role_ref: RoleRef,
110 #[serde(default)]
111 pub subjects: Vec<Subject>,
112}
113
114impl ConfigValidator for K8sRoleBinding {
115 fn yaml_type(&self) -> YamlType {
116 if self.kind == "ClusterRoleBinding" { YamlType::K8sClusterRoleBinding } else { YamlType::K8sRoleBinding }
117 }
118
119 fn validate_structure(&self) -> Vec<Diagnostic> {
120 let mut diags = Vec::new();
121 if self.subjects.is_empty() {
122 diags.push(Diagnostic {
123 severity: Severity::Error,
124 message: "No subjects defined — binding won't grant access to anyone".into(),
125 path: Some("subjects".into()),
126 });
127 }
128 if self.role_ref.name.is_empty() {
129 diags.push(Diagnostic {
130 severity: Severity::Error,
131 message: "roleRef.name cannot be empty".into(),
132 path: Some("roleRef > name".into()),
133 });
134 }
135 diags
136 }
137
138 fn validate_semantics(&self) -> Vec<Diagnostic> {
139 let mut diags = Vec::new();
140 if self.kind == "ClusterRoleBinding" && self.role_ref.name == "cluster-admin" {
142 diags.push(Diagnostic {
143 severity: Severity::Warning,
144 message: "Binding to cluster-admin grants full cluster access — use with caution".into(),
145 path: Some("roleRef > name".into()),
146 });
147 }
148 for (i, subj) in self.subjects.iter().enumerate() {
149 if subj.kind == "ServiceAccount" && subj.namespace.is_none() && self.kind == "ClusterRoleBinding" {
150 diags.push(Diagnostic {
151 severity: Severity::Info,
152 message: format!("Subject[{}]: ServiceAccount '{}' has no namespace — defaults to 'default'", i, subj.name),
153 path: Some(format!("subjects > {}", i)),
154 });
155 }
156 }
157 diags
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ServiceAccountSecret {
167 pub name: String,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct K8sServiceAccount {
173 #[serde(rename = "apiVersion")]
174 pub api_version: String,
175 pub kind: String,
176 pub metadata: K8sMetadata,
177 #[serde(default)]
178 pub secrets: Vec<ServiceAccountSecret>,
179 #[serde(default, rename = "imagePullSecrets")]
180 pub image_pull_secrets: Vec<ServiceAccountSecret>,
181 #[serde(default, rename = "automountServiceAccountToken")]
182 pub automount_service_account_token: Option<bool>,
183}
184
185impl ConfigValidator for K8sServiceAccount {
186 fn yaml_type(&self) -> YamlType { YamlType::K8sServiceAccount }
187
188 fn validate_structure(&self) -> Vec<Diagnostic> {
189 vec![]
190 }
191
192 fn validate_semantics(&self) -> Vec<Diagnostic> {
193 let mut diags = Vec::new();
194 if self.automount_service_account_token != Some(false) {
195 diags.push(Diagnostic {
196 severity: Severity::Info,
197 message: "automountServiceAccountToken is not explicitly false — token will be mounted into pods".into(),
198 path: Some("automountServiceAccountToken".into()),
199 });
200 }
201 diags
202 }
203}