Skip to main content

devops_models/models/
k8s_rbac.rs

1use serde::{Deserialize, Serialize};
2use crate::models::k8s::K8sMetadata;
3use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
4
5// ═══════════════════════════════════════════════════════════════════════════
6// Role / ClusterRole
7// ═══════════════════════════════════════════════════════════════════════════
8
9#[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// ═══════════════════════════════════════════════════════════════════════════
78// RoleBinding / ClusterRoleBinding
79// ═══════════════════════════════════════════════════════════════════════════
80
81#[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        // Warn about ClusterRoleBinding with cluster-admin
141        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// ═══════════════════════════════════════════════════════════════════════════
162// ServiceAccount
163// ═══════════════════════════════════════════════════════════════════════════
164
165#[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}