use serde::{Deserialize, Serialize};
use crate::models::k8s::K8sMetadata;
use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyRule {
#[serde(default, rename = "apiGroups")]
pub api_groups: Vec<String>,
#[serde(default)]
pub resources: Vec<String>,
pub verbs: Vec<String>,
#[serde(default, rename = "resourceNames")]
pub resource_names: Vec<String>,
#[serde(default, rename = "nonResourceURLs")]
pub non_resource_urls: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sRole {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub rules: Vec<PolicyRule>,
}
impl ConfigValidator for K8sRole {
fn yaml_type(&self) -> YamlType {
if self.kind == "ClusterRole" { YamlType::K8sClusterRole } else { YamlType::K8sRole }
}
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
for (i, rule) in self.rules.iter().enumerate() {
if rule.verbs.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Rule[{}]: verbs cannot be empty", i),
path: Some(format!("rules > {}", i)),
});
}
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
for (i, rule) in self.rules.iter().enumerate() {
if rule.resources.contains(&"*".to_string()) || rule.verbs.contains(&"*".to_string()) {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Rule[{}]: wildcard '*' grants broad access — follow least-privilege principle", i),
path: Some(format!("rules > {}", i)),
});
}
let dangerous_verbs = ["delete", "deletecollection", "escalate", "impersonate"];
for verb in &rule.verbs {
if dangerous_verbs.contains(&verb.as_str()) {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Rule[{}]: verb '{}' is potentially dangerous", i, verb),
path: Some(format!("rules > {} > verbs", i)),
});
}
}
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RoleRef {
#[serde(rename = "apiGroup")]
pub api_group: String,
pub kind: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Subject {
pub kind: String,
pub name: String,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default, rename = "apiGroup")]
pub api_group: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sRoleBinding {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
#[serde(rename = "roleRef")]
pub role_ref: RoleRef,
#[serde(default)]
pub subjects: Vec<Subject>,
}
impl ConfigValidator for K8sRoleBinding {
fn yaml_type(&self) -> YamlType {
if self.kind == "ClusterRoleBinding" { YamlType::K8sClusterRoleBinding } else { YamlType::K8sRoleBinding }
}
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.subjects.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "No subjects defined — binding won't grant access to anyone".into(),
path: Some("subjects".into()),
});
}
if self.role_ref.name.is_empty() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "roleRef.name cannot be empty".into(),
path: Some("roleRef > name".into()),
});
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.kind == "ClusterRoleBinding" && self.role_ref.name == "cluster-admin" {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "Binding to cluster-admin grants full cluster access — use with caution".into(),
path: Some("roleRef > name".into()),
});
}
for (i, subj) in self.subjects.iter().enumerate() {
if subj.kind == "ServiceAccount" && subj.namespace.is_none() && self.kind == "ClusterRoleBinding" {
diags.push(Diagnostic {
severity: Severity::Info,
message: format!("Subject[{}]: ServiceAccount '{}' has no namespace — defaults to 'default'", i, subj.name),
path: Some(format!("subjects > {}", i)),
});
}
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceAccountSecret {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sServiceAccount {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
#[serde(default)]
pub secrets: Vec<ServiceAccountSecret>,
#[serde(default, rename = "imagePullSecrets")]
pub image_pull_secrets: Vec<ServiceAccountSecret>,
#[serde(default, rename = "automountServiceAccountToken")]
pub automount_service_account_token: Option<bool>,
}
impl ConfigValidator for K8sServiceAccount {
fn yaml_type(&self) -> YamlType { YamlType::K8sServiceAccount }
fn validate_structure(&self) -> Vec<Diagnostic> {
vec![]
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.automount_service_account_token != Some(false) {
diags.push(Diagnostic {
severity: Severity::Info,
message: "automountServiceAccountToken is not explicitly false — token will be mounted into pods".into(),
path: Some("automountServiceAccountToken".into()),
});
}
diags
}
}