From ab9f00ff0ee8b08f98b31bb83e7c8811518f6e13 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Wed, 31 Jul 2024 18:20:45 +0200 Subject: [PATCH 1/2] refactor: split the `scaffold.rs` file into a module The `scaffold.rs` file was becoming too big. A module has been created, populated by one file per scaffold sub-command. Signed-off-by: Flavio Castelli --- src/scaffold.rs | 823 +--------------------------- src/scaffold/artifacthub.rs | 43 ++ src/scaffold/kubewarden_crds.rs | 64 +++ src/scaffold/manifest.rs | 461 ++++++++++++++++ src/scaffold/vap.rs | 232 ++++++++ src/scaffold/verification_config.rs | 43 ++ 6 files changed, 852 insertions(+), 814 deletions(-) create mode 100644 src/scaffold/artifacthub.rs create mode 100644 src/scaffold/kubewarden_crds.rs create mode 100644 src/scaffold/manifest.rs create mode 100644 src/scaffold/vap.rs create mode 100644 src/scaffold/verification_config.rs diff --git a/src/scaffold.rs b/src/scaffold.rs index 1a12cc56..20970fff 100644 --- a/src/scaffold.rs +++ b/src/scaffold.rs @@ -1,818 +1,13 @@ -use anyhow::{anyhow, Result}; -use k8s_openapi::api::admissionregistration::v1::{ - ValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding, -}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta}; -use policy_evaluator::{policy_fetcher::oci_distribution::Reference, validator::Validate}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - convert::TryFrom, - fs::{self, File}, - path::{Path, PathBuf}, - str::FromStr, -}; -use time::OffsetDateTime; -use tracing::warn; +mod kubewarden_crds; -use policy_evaluator::constants::{ - KUBEWARDEN_ANNOTATION_POLICY_CATEGORY, KUBEWARDEN_ANNOTATION_POLICY_SEVERITY, - KUBEWARDEN_ANNOTATION_POLICY_TITLE, -}; -use policy_evaluator::policy_artifacthub::ArtifactHubPkg; -use policy_evaluator::policy_fetcher::verify::config::{ - LatestVerificationConfig, Signature, VersionedVerificationConfig, -}; -use policy_evaluator::policy_metadata::{ContextAwareResource, Metadata, Rule}; +mod manifest; +pub(crate) use manifest::manifest; -pub(crate) enum ManifestType { - ClusterAdmissionPolicy, - AdmissionPolicy, -} +mod vap; +pub(crate) use vap::vap; -impl FromStr for ManifestType { - type Err = anyhow::Error; +mod verification_config; +pub(crate) use verification_config::verification_config; - fn from_str(value: &str) -> std::result::Result { - match value { - "ClusterAdmissionPolicy" => Ok(ManifestType::ClusterAdmissionPolicy), - "AdmissionPolicy" => Ok(ManifestType::AdmissionPolicy), - _ => Err(anyhow!("unknown manifest type")), - } - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ClusterAdmissionPolicy { - api_version: String, - kind: String, - metadata: ObjectMeta, - spec: ClusterAdmissionPolicySpec, -} - -#[derive(Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -struct ClusterAdmissionPolicySpec { - module: String, - settings: serde_yaml::Mapping, - rules: Vec, - mutating: bool, - // Skip serialization when this is true, which is the default case. - // This is needed as a temporary fix for https://github.com/kubewarden/kubewarden-controller/issues/395 - #[serde(skip_serializing_if = "is_true")] - background_audit: bool, - #[serde(skip_serializing_if = "BTreeSet::is_empty")] - context_aware_resources: BTreeSet, - #[serde(skip_serializing_if = "Option::is_none")] - failure_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - match_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - namespace_selector: Option, - #[serde(skip_serializing_if = "Option::is_none")] - object_selector: Option, -} - -fn is_true(b: &bool) -> bool { - *b -} - -impl TryFrom for ClusterAdmissionPolicy { - type Error = anyhow::Error; - - fn try_from(data: ScaffoldPolicyData) -> Result { - data.metadata.validate()?; - Ok(ClusterAdmissionPolicy { - api_version: String::from("policies.kubewarden.io/v1"), - kind: String::from("ClusterAdmissionPolicy"), - metadata: build_objmetadata(data.clone()), - spec: ClusterAdmissionPolicySpec { - module: data.uri, - settings: data.settings, - rules: data.metadata.rules.clone(), - mutating: data.metadata.mutating, - background_audit: data.metadata.background_audit, - context_aware_resources: data.metadata.context_aware_resources, - ..Default::default() - }, - }) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AdmissionPolicy { - api_version: String, - kind: String, - metadata: ObjectMeta, - spec: AdmissionPolicySpec, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AdmissionPolicySpec { - module: String, - settings: serde_yaml::Mapping, - rules: Vec, - mutating: bool, - // Skip serialization when this is true, which is the default case. - // This is needed as a temporary fix for https://github.com/kubewarden/kubewarden-controller/issues/395 - #[serde(skip_serializing_if = "is_true")] - background_audit: bool, -} - -impl TryFrom for AdmissionPolicy { - type Error = anyhow::Error; - - fn try_from(data: ScaffoldPolicyData) -> Result { - data.metadata.validate()?; - Ok(AdmissionPolicy { - api_version: String::from("policies.kubewarden.io/v1"), - kind: String::from("AdmissionPolicy"), - metadata: build_objmetadata(data.clone()), - spec: AdmissionPolicySpec { - module: data.uri, - settings: data.settings, - rules: data.metadata.rules.clone(), - mutating: data.metadata.mutating, - background_audit: data.metadata.background_audit, - }, - }) - } -} - -#[derive(Clone)] -struct ScaffoldPolicyData { - pub uri: String, - policy_title: Option, - metadata: Metadata, - settings: serde_yaml::Mapping, -} - -pub(crate) fn manifest( - uri_or_sha_prefix: &str, - resource_type: ManifestType, - settings: Option<&str>, - policy_title: Option<&str>, - allow_context_aware_resources: bool, -) -> Result<()> { - let uri = crate::utils::map_path_to_uri(uri_or_sha_prefix)?; - let wasm_path = crate::utils::wasm_path(&uri)?; - - let metadata = Metadata::from_path(&wasm_path)? - .ok_or_else(|| - anyhow!( - "No Kubewarden metadata found inside of '{}'.\nPolicies can be annotated with the `kwctl annotate` command.", - uri) - )?; - - let settings_yml: serde_yaml::Mapping = serde_yaml::from_str(settings.unwrap_or("{}"))?; - - let scaffold_data = ScaffoldPolicyData { - uri, - policy_title: get_policy_title_from_cli_or_metadata(policy_title, &metadata), - metadata, - settings: settings_yml, - }; - - let resource = - generate_yaml_resource(scaffold_data, resource_type, allow_context_aware_resources)?; - - let stdout = std::io::stdout(); - let out = stdout.lock(); - serde_yaml::to_writer(out, &resource)?; - - Ok(()) -} - -fn generate_yaml_resource( - scaffold_data: ScaffoldPolicyData, - resource_type: ManifestType, - allow_context_aware_resources: bool, -) -> Result { - let mut scaffold_data = scaffold_data; - - match resource_type { - ManifestType::ClusterAdmissionPolicy => { - if !scaffold_data.metadata.context_aware_resources.is_empty() { - if allow_context_aware_resources { - warn!( - "Policy has been granted access to the Kubernetes resources mentioned by its metadata." - ); - warn!("Carefully review the contents of the `contextAwareResources` attribute for abuses."); - } else { - warn!("Policy requires access to Kubernetes resources at evaluation time. For safety resons, the `contextAwareResources` attribute has been left empty."); - warn!("Carefully review which types of Kubernetes resources the policy needs via the `inspect` command an populate the `contextAwareResources` accordingly."); - warn!("Otherwise, invoke the `scaffold` command using the `--allow-context-aware` flag."); - - scaffold_data.metadata.context_aware_resources = BTreeSet::new(); - } - } - - serde_yaml::to_value(ClusterAdmissionPolicy::try_from(scaffold_data)?) - .map_err(|e| anyhow!("{}", e)) - } - ManifestType::AdmissionPolicy => { - serde_yaml::to_value(AdmissionPolicy::try_from(scaffold_data)?) - .map_err(|e| anyhow!("{}", e)) - } - } -} - -fn get_policy_title_from_cli_or_metadata( - policy_title: Option<&str>, - metadata: &Metadata, -) -> Option { - policy_title.map(|t| t.to_string()).or_else(|| { - metadata - .annotations - .as_ref() - .unwrap_or(&HashMap::new()) - .get(KUBEWARDEN_ANNOTATION_POLICY_TITLE) - .map(|s| s.to_string()) - }) -} - -fn build_objmetadata(data: ScaffoldPolicyData) -> ObjectMeta { - let mut annots: BTreeMap = BTreeMap::new(); - if let Some(an) = data.metadata.annotations { - if let Some(severity) = an.get(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY) { - annots.insert( - String::from(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY), - severity.to_owned(), - ); - } - if let Some(category) = an.get(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY) { - annots.insert( - String::from(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY), - category.to_owned(), - ); - } - } - - let annots_option: Option> = match !annots.is_empty() { - true => Some(annots), - false => None, - }; - - ObjectMeta { - name: data.policy_title, - annotations: annots_option, - ..Default::default() - } -} - -pub(crate) fn verification_config() -> Result { - let mut comment_header = r#"# Default Kubewarden verification config -# -# With this config, the only valid policies are those signed by Kubewarden -# infrastructure. -# -# This config can be saved to its default location (for this OS) with: -# kwctl scaffold verification-config > "# - .to_string(); - - comment_header.push_str( - super::KWCTL_DEFAULT_VERIFICATION_CONFIG_PATH - .to_owned() - .as_str(), - ); - comment_header.push_str( - r#" -# -# Providing a config in the default location enables Sigstore verification. -# See https://docs.kubewarden.io for more Sigstore verification options."#, - ); - - let kubewarden_verification_config = - VersionedVerificationConfig::V1(LatestVerificationConfig { - all_of: Some(vec![Signature::GithubAction { - owner: "kubewarden".to_string(), - repo: None, - annotations: None, - }]), - any_of: None, - }); - - Ok(format!( - "{}\n{}", - comment_header, - serde_yaml::to_string(&kubewarden_verification_config)? - )) -} - -pub(crate) fn artifacthub( - metadata_path: PathBuf, - version: &str, - questions_path: Option, -) -> Result { - let comment_header = r#"# Kubewarden Artifacthub Package config -# -# Use this config to submit the policy to https://artifacthub.io. -# -# This config can be saved to its default location with: -# kwctl scaffold artifacthub > artifacthub-pkg.yml "#; - - let metadata_file = - File::open(metadata_path).map_err(|e| anyhow!("Error opening metadata file: {}", e))?; - let metadata: Metadata = serde_yaml::from_reader(&metadata_file) - .map_err(|e| anyhow!("Error unmarshalling metadata {}", e))?; - let questions = questions_path - .map(|path| { - fs::read_to_string(path).map_err(|e| anyhow!("Error reading questions file: {}", e)) - }) - .transpose()?; - - let kubewarden_artifacthub_pkg = ArtifactHubPkg::from_metadata( - &metadata, - version, - OffsetDateTime::now_utc(), - questions.as_deref(), - )?; - - Ok(format!( - "{}\n{}", - comment_header, - serde_yaml::to_string(&kubewarden_artifacthub_pkg)? - )) -} - -pub(crate) fn vap(cel_policy_module: &str, vap_path: &Path, binding_path: &Path) -> Result<()> { - let vap_file = File::open(vap_path) - .map_err(|e| anyhow!("cannot open {}: #{e}", vap_path.to_str().unwrap()))?; - let binding_file = File::open(binding_path) - .map_err(|e| anyhow!("cannot open {}: #{e}", binding_path.to_str().unwrap()))?; - - let vap: ValidatingAdmissionPolicy = serde_yaml::from_reader(vap_file) - .map_err(|e| anyhow!("cannot convert given data into a ValidatingAdmissionPolicy: #{e}"))?; - let vap_binding: ValidatingAdmissionPolicyBinding = serde_yaml::from_reader(binding_file) - .map_err(|e| { - anyhow!("cannot convert given data into a ValidatingAdmissionPolicyBinding: #{e}") - })?; - - match cel_policy_module.parse::() { - Ok(cel_policy_ref) => match cel_policy_ref.tag() { - None | Some("latest") => { - warn!( - "Using the 'latest' version of the CEL policy could lead to unexpected behavior. It is recommended to use a specific version to avoid breaking changes." - ); - } - _ => {} - }, - Err(_) => { - warn!("The CEL policy module specified is not a valid OCI reference"); - } - } - - let cluster_admission_policy = - convert_vap_to_cluster_admission_policy(cel_policy_module, vap, vap_binding)?; - - serde_yaml::to_writer(std::io::stdout(), &cluster_admission_policy)?; - - Ok(()) -} - -fn convert_vap_to_cluster_admission_policy( - cel_policy_module: &str, - vap: ValidatingAdmissionPolicy, - vap_binding: ValidatingAdmissionPolicyBinding, -) -> anyhow::Result { - let vap_spec = vap.spec.unwrap_or_default(); - if vap_spec.audit_annotations.is_some() { - warn!("auditAnnotations are not supported by Kubewarden's CEL policy yet. They will be ignored."); - } - if vap_spec.match_conditions.is_some() { - warn!("matchConditions are not supported by Kubewarden's CEL policy yet. They will be ignored."); - } - if vap_spec.param_kind.is_some() { - // It's not safe to skip this, the policy will definitely not work. - return Err(anyhow!( - "paramKind is not supported by Kubewarden's CEL policy yet" - )); - } - - let mut settings = serde_yaml::Mapping::new(); - - // migrate CEL variables - if let Some(vap_variables) = vap_spec.variables { - let vap_variables: Vec = vap_variables - .iter() - .map(|v| serde_yaml::to_value(v).expect("cannot convert VAP variable to YAML")) - .collect(); - settings.insert("variables".into(), vap_variables.into()); - } - - // migrate CEL validations - if let Some(vap_validations) = vap_spec.validations { - let kw_cel_validations: Vec = vap_validations - .iter() - .map(|v| serde_yaml::to_value(v).expect("cannot convert VAP validation to YAML")) - .collect(); - settings.insert("validations".into(), kw_cel_validations.into()); - } - - // VAP specifies the namespace selector inside of the binding - let namespace_selector = vap_binding - .spec - .unwrap_or_default() - .match_resources - .unwrap_or_default() - .namespace_selector; - - // VAP rules are specified inside of the VAP object - let vap_match_constraints = vap_spec.match_constraints.unwrap_or_default(); - let match_policy = vap_match_constraints.match_policy; - let rules = vap_match_constraints - .resource_rules - .unwrap_or_default() - .iter() - .map(Rule::try_from) - .collect::, &'static str>>() - .map_err(|e| anyhow!("error converting VAP matchConstraints into rules: {e}"))?; - - // migrate VAP - let cluster_admission_policy = ClusterAdmissionPolicy { - api_version: "policies.kubewarden.io/v1".to_string(), - kind: "ClusterAdmissionPolicy".to_string(), - metadata: vap_binding.metadata, - spec: ClusterAdmissionPolicySpec { - module: cel_policy_module.to_string(), - namespace_selector, - match_policy, - rules, - object_selector: vap_match_constraints.object_selector, - mutating: false, - background_audit: true, - context_aware_resources: BTreeSet::new(), - failure_policy: vap_spec.failure_policy, - mode: None, // VAP policies are always in protect mode, which is the default for KW - settings, - }, - }; - - Ok(cluster_admission_policy) -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::*; - - pub fn test_data(path: &str) -> String { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("data") - .join(path) - .to_string_lossy() - .to_string() - } - - const CEL_POLICY_MODULE: &str = "ghcr.io/kubewarden/policies/cel-policy:latest"; - - fn mock_metadata_with_no_annotations() -> Metadata { - Metadata { - protocol_version: None, - rules: vec![], - annotations: None, - mutating: false, - background_audit: true, - context_aware_resources: BTreeSet::new(), - execution_mode: Default::default(), - policy_type: Default::default(), - minimum_kubewarden_version: None, - } - } - - fn mock_metadata_with_title(title: &str) -> Metadata { - Metadata { - protocol_version: None, - rules: vec![], - annotations: Some(HashMap::from([( - KUBEWARDEN_ANNOTATION_POLICY_TITLE.to_string(), - title.to_string(), - )])), - mutating: false, - background_audit: true, - context_aware_resources: BTreeSet::new(), - execution_mode: Default::default(), - policy_type: Default::default(), - minimum_kubewarden_version: None, - } - } - - fn mock_metadata_with_severity_category() -> Metadata { - Metadata { - protocol_version: None, - rules: vec![], - annotations: Some(HashMap::from([ - ( - KUBEWARDEN_ANNOTATION_POLICY_TITLE.to_string(), - String::from("test"), - ), - ( - KUBEWARDEN_ANNOTATION_POLICY_SEVERITY.to_string(), - String::from("medium"), - ), - ( - KUBEWARDEN_ANNOTATION_POLICY_CATEGORY.to_string(), - String::from("PSP"), - ), - ])), - mutating: false, - background_audit: true, - context_aware_resources: BTreeSet::new(), - execution_mode: Default::default(), - policy_type: Default::default(), - minimum_kubewarden_version: None, - } - } - - #[test] - fn get_policy_title_from_cli_or_metadata_returns_name_from_cli_if_present() { - let policy_title = "name"; - assert_eq!( - Some(policy_title.to_string()), - get_policy_title_from_cli_or_metadata( - Some(policy_title), - &mock_metadata_with_no_annotations() - ) - ) - } - - #[test] - fn get_policy_title_from_cli_or_metadata_returns_none_if_both_are_missing() { - assert_eq!( - None, - get_policy_title_from_cli_or_metadata(None, &mock_metadata_with_no_annotations()) - ) - } - - #[test] - fn get_policy_title_from_cli_or_metadata_returns_title_from_annotation_if_name_from_cli_not_present( - ) { - let policy_title = "title"; - assert_eq!( - Some(policy_title.to_string()), - get_policy_title_from_cli_or_metadata(None, &mock_metadata_with_title(policy_title)) - ) - } - - #[test] - fn build_objmetadata_when_no_annotation() { - let mut metadata = mock_metadata_with_no_annotations(); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: Some("test".to_string()), - metadata, - settings: Default::default(), - }; - - let obj_metadata = build_objmetadata(scaffold_data); - assert!(obj_metadata.annotations.is_none()); - } - - #[test] - fn build_objmetadata_with_annot_severity_category() { - let mut metadata = mock_metadata_with_severity_category(); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: Some("test".to_string()), - metadata, - settings: Default::default(), - }; - - let obj_metadata = build_objmetadata(scaffold_data); - assert_eq!( - obj_metadata - .annotations - .as_ref() - .expect("we should have annotations") - .get(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY) - .expect("we should have severity"), - &String::from("medium") - ); - assert_eq!( - obj_metadata - .annotations - .as_ref() - .expect("we should have annotations") - .get(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY) - .expect("we should have category"), - &String::from("PSP") - ); - } - - #[test] - fn omit_background_audit_during_serialization_when_true() { - // testing fix for https://github.com/kubewarden/kubewarden-controller/issues/395 - let policy_title = "test"; - let mut metadata = mock_metadata_with_title(policy_title); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - assert!(metadata.background_audit); - - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), - metadata, - settings: Default::default(), - }; - - let out = serde_yaml::to_string( - &ClusterAdmissionPolicy::try_from(scaffold_data.clone()) - .expect("cannot build ClusterAdmissionPolicy"), - ) - .expect("serialization error"); - assert!(!out.contains("backgroundAudit")); - - let out = serde_yaml::to_string( - &AdmissionPolicy::try_from(scaffold_data).expect("cannot build AdmissionPolicy"), - ) - .expect("serialization error"); - assert!(!out.contains("backgroundAudit")); - } - - #[test] - fn do_not_omit_background_audit_during_serialization_when_false() { - // testing fix for https://github.com/kubewarden/kubewarden-controller/issues/395 - let policy_title = "test"; - let mut metadata = mock_metadata_with_title(policy_title); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - metadata.background_audit = false; - assert!(!metadata.background_audit); - - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), - metadata, - settings: Default::default(), - }; - - let out = serde_yaml::to_string( - &ClusterAdmissionPolicy::try_from(scaffold_data.clone()) - .expect("cannot build ClusterAdmissionPolicy"), - ) - .expect("serialization error"); - assert!(out.contains("backgroundAudit")); - - let out = serde_yaml::to_string( - &AdmissionPolicy::try_from(scaffold_data).expect("cannot build AdmissionPolicy"), - ) - .expect("serialization error"); - assert!(out.contains("backgroundAudit")); - } - - #[test] - fn scaffold_cluster_admission_policy_with_context_aware_enabled() { - let mut context_aware_resources: BTreeSet = BTreeSet::new(); - context_aware_resources.insert(ContextAwareResource { - api_version: "v1".to_string(), - kind: "Pod".to_string(), - }); - - let policy_title = "test"; - let mut metadata = mock_metadata_with_title(policy_title); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - metadata.context_aware_resources = context_aware_resources; - - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), - metadata, - settings: Default::default(), - }; - - let resource = - generate_yaml_resource(scaffold_data, ManifestType::ClusterAdmissionPolicy, true) - .expect("Cannot create yaml resource"); - - let resource = resource.as_mapping().expect("resource should be a Map"); - let spec = resource.get("spec").expect("cannot get `Spec`"); - let context_aware_resources = spec.get("contextAwareResources"); - assert!(context_aware_resources.is_some()); - } - - #[test] - fn scaffold_cluster_admission_policy_with_context_aware_disabled() { - let mut context_aware_resources: BTreeSet = BTreeSet::new(); - context_aware_resources.insert(ContextAwareResource { - api_version: "v1".to_string(), - kind: "Pod".to_string(), - }); - - let policy_title = "test"; - let mut metadata = mock_metadata_with_title(policy_title); - metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); - metadata.context_aware_resources = context_aware_resources; - - let scaffold_data = ScaffoldPolicyData { - uri: "not_relevant".to_string(), - policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), - metadata, - settings: Default::default(), - }; - - let resource = - generate_yaml_resource(scaffold_data, ManifestType::ClusterAdmissionPolicy, false) - .expect("Cannot create yaml resource"); - - let resource = resource.as_mapping().expect("resource should be a Map"); - let spec = resource.get("spec").expect("cannot get `Spec`"); - let context_aware_resources = spec.get("contextAwareResources"); - assert!(context_aware_resources.is_none()); - } - - #[rstest] - #[case::vap_without_variables("vap/vap-without-variables.yml", "vap/vap-binding.yml", false)] - #[case::vap_with_variables("vap/vap-with-variables.yml", "vap/vap-binding.yml", true)] - fn from_vap_to_cluster_admission_policy( - #[case] vap_yaml_path: &str, - #[case] vap_binding_yaml_path: &str, - #[case] has_variables: bool, - ) { - let yaml_file = File::open(test_data(vap_yaml_path)).unwrap(); - let vap: ValidatingAdmissionPolicy = serde_yaml::from_reader(yaml_file).unwrap(); - - let expected_validations = - serde_yaml::to_value(vap.clone().spec.unwrap().validations.unwrap()).unwrap(); - let expected_rules = vap - .clone() - .spec - .unwrap() - .match_constraints - .unwrap() - .resource_rules - .unwrap() - .iter() - .map(Rule::try_from) - .collect::, &str>>() - .unwrap(); - - let yaml_file = File::open(test_data(vap_binding_yaml_path)).unwrap(); - let vap_binding: ValidatingAdmissionPolicyBinding = - serde_yaml::from_reader(yaml_file).unwrap(); - - let cluster_admission_policy = convert_vap_to_cluster_admission_policy( - CEL_POLICY_MODULE, - vap.clone(), - vap_binding.clone(), - ) - .unwrap(); - - assert_eq!(CEL_POLICY_MODULE, cluster_admission_policy.spec.module); - assert!(!cluster_admission_policy.spec.mutating); - assert_eq!(cluster_admission_policy.spec.rules, expected_rules); - assert!(cluster_admission_policy.spec.background_audit); - assert!(cluster_admission_policy - .spec - .context_aware_resources - .is_empty()); - assert_eq!( - vap.clone().spec.unwrap().failure_policy, - cluster_admission_policy.spec.failure_policy - ); - assert!(cluster_admission_policy.spec.mode.is_none()); - assert_eq!( - vap.clone() - .spec - .unwrap() - .match_constraints - .unwrap() - .match_policy, - cluster_admission_policy.spec.match_policy - ); - assert_eq!( - vap_binding - .clone() - .spec - .unwrap() - .match_resources - .unwrap() - .namespace_selector, - cluster_admission_policy.spec.namespace_selector - ); - assert!(cluster_admission_policy.spec.object_selector.is_none()); - assert_eq!( - expected_validations, - cluster_admission_policy.spec.settings["validations"] - ); - - if has_variables { - let expected_variables = - serde_yaml::to_value(vap.clone().spec.unwrap().variables.unwrap()).unwrap(); - assert_eq!( - expected_variables, - cluster_admission_policy.spec.settings["variables"] - ); - } else { - assert!(!cluster_admission_policy - .spec - .settings - .contains_key("variables")); - } - } -} +mod artifacthub; +pub(crate) use artifacthub::artifacthub; diff --git a/src/scaffold/artifacthub.rs b/src/scaffold/artifacthub.rs new file mode 100644 index 00000000..adc99135 --- /dev/null +++ b/src/scaffold/artifacthub.rs @@ -0,0 +1,43 @@ +use anyhow::{anyhow, Result}; +use policy_evaluator::{policy_artifacthub::ArtifactHubPkg, policy_metadata::Metadata}; +use std::{ + fs::{self, File}, + path::PathBuf, +}; +use time::OffsetDateTime; + +pub(crate) fn artifacthub( + metadata_path: PathBuf, + version: &str, + questions_path: Option, +) -> Result { + let comment_header = r#"# Kubewarden Artifacthub Package config +# +# Use this config to submit the policy to https://artifacthub.io. +# +# This config can be saved to its default location with: +# kwctl scaffold artifacthub > artifacthub-pkg.yml "#; + + let metadata_file = + File::open(metadata_path).map_err(|e| anyhow!("Error opening metadata file: {}", e))?; + let metadata: Metadata = serde_yaml::from_reader(&metadata_file) + .map_err(|e| anyhow!("Error unmarshalling metadata {}", e))?; + let questions = questions_path + .map(|path| { + fs::read_to_string(path).map_err(|e| anyhow!("Error reading questions file: {}", e)) + }) + .transpose()?; + + let kubewarden_artifacthub_pkg = ArtifactHubPkg::from_metadata( + &metadata, + version, + OffsetDateTime::now_utc(), + questions.as_deref(), + )?; + + Ok(format!( + "{}\n{}", + comment_header, + serde_yaml::to_string(&kubewarden_artifacthub_pkg)? + )) +} diff --git a/src/scaffold/kubewarden_crds.rs b/src/scaffold/kubewarden_crds.rs new file mode 100644 index 00000000..747b0c88 --- /dev/null +++ b/src/scaffold/kubewarden_crds.rs @@ -0,0 +1,64 @@ +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta}; +use policy_evaluator::policy_metadata::{ContextAwareResource, Rule}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClusterAdmissionPolicy { + pub api_version: String, + pub kind: String, + pub metadata: ObjectMeta, + pub spec: ClusterAdmissionPolicySpec, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClusterAdmissionPolicySpec { + pub module: String, + pub settings: serde_yaml::Mapping, + pub rules: Vec, + pub mutating: bool, + // Skip serialization when this is true, which is the default case. + // This is needed as a temporary fix for https://github.com/kubewarden/kubewarden-controller/issues/395 + #[serde(skip_serializing_if = "is_true")] + pub background_audit: bool, + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + pub context_aware_resources: BTreeSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub failure_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub match_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace_selector: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_selector: Option, +} + +fn is_true(b: &bool) -> bool { + *b +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AdmissionPolicy { + pub api_version: String, + pub kind: String, + pub metadata: ObjectMeta, + pub spec: AdmissionPolicySpec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AdmissionPolicySpec { + pub module: String, + pub settings: serde_yaml::Mapping, + pub rules: Vec, + pub mutating: bool, + // Skip serialization when this is true, which is the default case. + // This is needed as a temporary fix for https://github.com/kubewarden/kubewarden-controller/issues/395 + #[serde(skip_serializing_if = "is_true")] + pub background_audit: bool, +} diff --git a/src/scaffold/manifest.rs b/src/scaffold/manifest.rs new file mode 100644 index 00000000..e1d85860 --- /dev/null +++ b/src/scaffold/manifest.rs @@ -0,0 +1,461 @@ +use anyhow::{anyhow, Result}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use policy_evaluator::{ + constants::{ + KUBEWARDEN_ANNOTATION_POLICY_CATEGORY, KUBEWARDEN_ANNOTATION_POLICY_SEVERITY, + KUBEWARDEN_ANNOTATION_POLICY_TITLE, + }, + policy_metadata::Metadata, + validator::Validate, +}; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + convert::TryFrom, + str::FromStr, +}; +use tracing::warn; + +use crate::scaffold::kubewarden_crds::{ + AdmissionPolicy, AdmissionPolicySpec, ClusterAdmissionPolicy, ClusterAdmissionPolicySpec, +}; + +pub(crate) enum ManifestType { + ClusterAdmissionPolicy, + AdmissionPolicy, +} + +impl FromStr for ManifestType { + type Err = anyhow::Error; + + fn from_str(value: &str) -> std::result::Result { + match value { + "ClusterAdmissionPolicy" => Ok(ManifestType::ClusterAdmissionPolicy), + "AdmissionPolicy" => Ok(ManifestType::AdmissionPolicy), + _ => Err(anyhow!("unknown manifest type")), + } + } +} + +#[derive(Clone)] +struct ScaffoldPolicyData { + pub uri: String, + policy_title: Option, + metadata: Metadata, + settings: serde_yaml::Mapping, +} + +impl TryFrom for ClusterAdmissionPolicy { + type Error = anyhow::Error; + + fn try_from(data: ScaffoldPolicyData) -> Result { + data.metadata.validate()?; + Ok(ClusterAdmissionPolicy { + api_version: String::from("policies.kubewarden.io/v1"), + kind: String::from("ClusterAdmissionPolicy"), + metadata: build_objmetadata(data.clone()), + spec: ClusterAdmissionPolicySpec { + module: data.uri, + settings: data.settings, + rules: data.metadata.rules.clone(), + mutating: data.metadata.mutating, + background_audit: data.metadata.background_audit, + context_aware_resources: data.metadata.context_aware_resources, + ..Default::default() + }, + }) + } +} + +impl TryFrom for AdmissionPolicy { + type Error = anyhow::Error; + + fn try_from(data: ScaffoldPolicyData) -> Result { + data.metadata.validate()?; + Ok(AdmissionPolicy { + api_version: String::from("policies.kubewarden.io/v1"), + kind: String::from("AdmissionPolicy"), + metadata: build_objmetadata(data.clone()), + spec: AdmissionPolicySpec { + module: data.uri, + settings: data.settings, + rules: data.metadata.rules.clone(), + mutating: data.metadata.mutating, + background_audit: data.metadata.background_audit, + }, + }) + } +} + +fn build_objmetadata(data: ScaffoldPolicyData) -> ObjectMeta { + let mut annots: BTreeMap = BTreeMap::new(); + if let Some(an) = data.metadata.annotations { + if let Some(severity) = an.get(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY) { + annots.insert( + String::from(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY), + severity.to_owned(), + ); + } + if let Some(category) = an.get(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY) { + annots.insert( + String::from(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY), + category.to_owned(), + ); + } + } + + let annots_option: Option> = match !annots.is_empty() { + true => Some(annots), + false => None, + }; + + ObjectMeta { + name: data.policy_title, + annotations: annots_option, + ..Default::default() + } +} + +pub(crate) fn manifest( + uri_or_sha_prefix: &str, + resource_type: ManifestType, + settings: Option<&str>, + policy_title: Option<&str>, + allow_context_aware_resources: bool, +) -> Result<()> { + let uri = crate::utils::map_path_to_uri(uri_or_sha_prefix)?; + let wasm_path = crate::utils::wasm_path(&uri)?; + + let metadata = Metadata::from_path(&wasm_path)? + .ok_or_else(|| + anyhow!( + "No Kubewarden metadata found inside of '{}'.\nPolicies can be annotated with the `kwctl annotate` command.", + uri) + )?; + + let settings_yml: serde_yaml::Mapping = serde_yaml::from_str(settings.unwrap_or("{}"))?; + + let scaffold_data = ScaffoldPolicyData { + uri, + policy_title: get_policy_title_from_cli_or_metadata(policy_title, &metadata), + metadata, + settings: settings_yml, + }; + + let resource = + generate_yaml_resource(scaffold_data, resource_type, allow_context_aware_resources)?; + + let stdout = std::io::stdout(); + let out = stdout.lock(); + serde_yaml::to_writer(out, &resource)?; + + Ok(()) +} + +fn get_policy_title_from_cli_or_metadata( + policy_title: Option<&str>, + metadata: &Metadata, +) -> Option { + policy_title.map(|t| t.to_string()).or_else(|| { + metadata + .annotations + .as_ref() + .unwrap_or(&HashMap::new()) + .get(KUBEWARDEN_ANNOTATION_POLICY_TITLE) + .map(|s| s.to_string()) + }) +} + +fn generate_yaml_resource( + scaffold_data: ScaffoldPolicyData, + resource_type: ManifestType, + allow_context_aware_resources: bool, +) -> Result { + let mut scaffold_data = scaffold_data; + + match resource_type { + ManifestType::ClusterAdmissionPolicy => { + if !scaffold_data.metadata.context_aware_resources.is_empty() { + if allow_context_aware_resources { + warn!( + "Policy has been granted access to the Kubernetes resources mentioned by its metadata." + ); + warn!("Carefully review the contents of the `contextAwareResources` attribute for abuses."); + } else { + warn!("Policy requires access to Kubernetes resources at evaluation time. For safety resons, the `contextAwareResources` attribute has been left empty."); + warn!("Carefully review which types of Kubernetes resources the policy needs via the `inspect` command an populate the `contextAwareResources` accordingly."); + warn!("Otherwise, invoke the `scaffold` command using the `--allow-context-aware` flag."); + + scaffold_data.metadata.context_aware_resources = BTreeSet::new(); + } + } + + serde_yaml::to_value(ClusterAdmissionPolicy::try_from(scaffold_data)?) + .map_err(|e| anyhow!("{}", e)) + } + ManifestType::AdmissionPolicy => { + serde_yaml::to_value(AdmissionPolicy::try_from(scaffold_data)?) + .map_err(|e| anyhow!("{}", e)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use policy_evaluator::policy_metadata::ContextAwareResource; + + fn mock_metadata_with_no_annotations() -> Metadata { + Metadata { + protocol_version: None, + rules: vec![], + annotations: None, + mutating: false, + background_audit: true, + context_aware_resources: BTreeSet::new(), + execution_mode: Default::default(), + policy_type: Default::default(), + minimum_kubewarden_version: None, + } + } + + fn mock_metadata_with_title(title: &str) -> Metadata { + Metadata { + protocol_version: None, + rules: vec![], + annotations: Some(HashMap::from([( + KUBEWARDEN_ANNOTATION_POLICY_TITLE.to_string(), + title.to_string(), + )])), + mutating: false, + background_audit: true, + context_aware_resources: BTreeSet::new(), + execution_mode: Default::default(), + policy_type: Default::default(), + minimum_kubewarden_version: None, + } + } + + fn mock_metadata_with_severity_category() -> Metadata { + Metadata { + protocol_version: None, + rules: vec![], + annotations: Some(HashMap::from([ + ( + KUBEWARDEN_ANNOTATION_POLICY_TITLE.to_string(), + String::from("test"), + ), + ( + KUBEWARDEN_ANNOTATION_POLICY_SEVERITY.to_string(), + String::from("medium"), + ), + ( + KUBEWARDEN_ANNOTATION_POLICY_CATEGORY.to_string(), + String::from("PSP"), + ), + ])), + mutating: false, + background_audit: true, + context_aware_resources: BTreeSet::new(), + execution_mode: Default::default(), + policy_type: Default::default(), + minimum_kubewarden_version: None, + } + } + + #[test] + fn get_policy_title_from_cli_or_metadata_returns_name_from_cli_if_present() { + let policy_title = "name"; + assert_eq!( + Some(policy_title.to_string()), + get_policy_title_from_cli_or_metadata( + Some(policy_title), + &mock_metadata_with_no_annotations() + ) + ) + } + + #[test] + fn get_policy_title_from_cli_or_metadata_returns_none_if_both_are_missing() { + assert_eq!( + None, + get_policy_title_from_cli_or_metadata(None, &mock_metadata_with_no_annotations()) + ) + } + + #[test] + fn get_policy_title_from_cli_or_metadata_returns_title_from_annotation_if_name_from_cli_not_present( + ) { + let policy_title = "title"; + assert_eq!( + Some(policy_title.to_string()), + get_policy_title_from_cli_or_metadata(None, &mock_metadata_with_title(policy_title)) + ) + } + + #[test] + fn build_objmetadata_when_no_annotation() { + let mut metadata = mock_metadata_with_no_annotations(); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: Some("test".to_string()), + metadata, + settings: Default::default(), + }; + + let obj_metadata = build_objmetadata(scaffold_data); + assert!(obj_metadata.annotations.is_none()); + } + + #[test] + fn build_objmetadata_with_annot_severity_category() { + let mut metadata = mock_metadata_with_severity_category(); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: Some("test".to_string()), + metadata, + settings: Default::default(), + }; + + let obj_metadata = build_objmetadata(scaffold_data); + assert_eq!( + obj_metadata + .annotations + .as_ref() + .expect("we should have annotations") + .get(KUBEWARDEN_ANNOTATION_POLICY_SEVERITY) + .expect("we should have severity"), + &String::from("medium") + ); + assert_eq!( + obj_metadata + .annotations + .as_ref() + .expect("we should have annotations") + .get(KUBEWARDEN_ANNOTATION_POLICY_CATEGORY) + .expect("we should have category"), + &String::from("PSP") + ); + } + + #[test] + fn omit_background_audit_during_serialization_when_true() { + // testing fix for https://github.com/kubewarden/kubewarden-controller/issues/395 + let policy_title = "test"; + let mut metadata = mock_metadata_with_title(policy_title); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + assert!(metadata.background_audit); + + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), + metadata, + settings: Default::default(), + }; + + let out = serde_yaml::to_string( + &ClusterAdmissionPolicy::try_from(scaffold_data.clone()) + .expect("cannot build ClusterAdmissionPolicy"), + ) + .expect("serialization error"); + assert!(!out.contains("backgroundAudit")); + + let out = serde_yaml::to_string( + &AdmissionPolicy::try_from(scaffold_data).expect("cannot build AdmissionPolicy"), + ) + .expect("serialization error"); + assert!(!out.contains("backgroundAudit")); + } + + #[test] + fn do_not_omit_background_audit_during_serialization_when_false() { + // testing fix for https://github.com/kubewarden/kubewarden-controller/issues/395 + let policy_title = "test"; + let mut metadata = mock_metadata_with_title(policy_title); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + metadata.background_audit = false; + assert!(!metadata.background_audit); + + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), + metadata, + settings: Default::default(), + }; + + let out = serde_yaml::to_string( + &ClusterAdmissionPolicy::try_from(scaffold_data.clone()) + .expect("cannot build ClusterAdmissionPolicy"), + ) + .expect("serialization error"); + assert!(out.contains("backgroundAudit")); + + let out = serde_yaml::to_string( + &AdmissionPolicy::try_from(scaffold_data).expect("cannot build AdmissionPolicy"), + ) + .expect("serialization error"); + assert!(out.contains("backgroundAudit")); + } + + #[test] + fn scaffold_cluster_admission_policy_with_context_aware_enabled() { + let mut context_aware_resources: BTreeSet = BTreeSet::new(); + context_aware_resources.insert(ContextAwareResource { + api_version: "v1".to_string(), + kind: "Pod".to_string(), + }); + + let policy_title = "test"; + let mut metadata = mock_metadata_with_title(policy_title); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + metadata.context_aware_resources = context_aware_resources; + + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), + metadata, + settings: Default::default(), + }; + + let resource = + generate_yaml_resource(scaffold_data, ManifestType::ClusterAdmissionPolicy, true) + .expect("Cannot create yaml resource"); + + let resource = resource.as_mapping().expect("resource should be a Map"); + let spec = resource.get("spec").expect("cannot get `Spec`"); + let context_aware_resources = spec.get("contextAwareResources"); + assert!(context_aware_resources.is_some()); + } + + #[test] + fn scaffold_cluster_admission_policy_with_context_aware_disabled() { + let mut context_aware_resources: BTreeSet = BTreeSet::new(); + context_aware_resources.insert(ContextAwareResource { + api_version: "v1".to_string(), + kind: "Pod".to_string(), + }); + + let policy_title = "test"; + let mut metadata = mock_metadata_with_title(policy_title); + metadata.protocol_version = Some(policy_evaluator::ProtocolVersion::V1); + metadata.context_aware_resources = context_aware_resources; + + let scaffold_data = ScaffoldPolicyData { + uri: "not_relevant".to_string(), + policy_title: get_policy_title_from_cli_or_metadata(Some(policy_title), &metadata), + metadata, + settings: Default::default(), + }; + + let resource = + generate_yaml_resource(scaffold_data, ManifestType::ClusterAdmissionPolicy, false) + .expect("Cannot create yaml resource"); + + let resource = resource.as_mapping().expect("resource should be a Map"); + let spec = resource.get("spec").expect("cannot get `Spec`"); + let context_aware_resources = spec.get("contextAwareResources"); + assert!(context_aware_resources.is_none()); + } +} diff --git a/src/scaffold/vap.rs b/src/scaffold/vap.rs new file mode 100644 index 00000000..d10c2b41 --- /dev/null +++ b/src/scaffold/vap.rs @@ -0,0 +1,232 @@ +use anyhow::{anyhow, Result}; +use k8s_openapi::api::admissionregistration::v1::{ + ValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding, +}; +use policy_evaluator::{policy_fetcher::oci_distribution::Reference, policy_metadata::Rule}; +use std::{collections::BTreeSet, convert::TryFrom, fs::File, path::Path}; +use tracing::warn; + +use crate::scaffold::kubewarden_crds::{ClusterAdmissionPolicy, ClusterAdmissionPolicySpec}; + +pub(crate) fn vap(cel_policy_module: &str, vap_path: &Path, binding_path: &Path) -> Result<()> { + let vap_file = File::open(vap_path) + .map_err(|e| anyhow!("cannot open {}: #{e}", vap_path.to_str().unwrap()))?; + let binding_file = File::open(binding_path) + .map_err(|e| anyhow!("cannot open {}: #{e}", binding_path.to_str().unwrap()))?; + + let vap: ValidatingAdmissionPolicy = serde_yaml::from_reader(vap_file) + .map_err(|e| anyhow!("cannot convert given data into a ValidatingAdmissionPolicy: #{e}"))?; + let vap_binding: ValidatingAdmissionPolicyBinding = serde_yaml::from_reader(binding_file) + .map_err(|e| { + anyhow!("cannot convert given data into a ValidatingAdmissionPolicyBinding: #{e}") + })?; + + match cel_policy_module.parse::() { + Ok(cel_policy_ref) => match cel_policy_ref.tag() { + None | Some("latest") => { + warn!( + "Using the 'latest' version of the CEL policy could lead to unexpected behavior. It is recommended to use a specific version to avoid breaking changes." + ); + } + _ => {} + }, + Err(_) => { + warn!("The CEL policy module specified is not a valid OCI reference"); + } + } + + let cluster_admission_policy = + convert_vap_to_cluster_admission_policy(cel_policy_module, vap, vap_binding)?; + + serde_yaml::to_writer(std::io::stdout(), &cluster_admission_policy)?; + + Ok(()) +} + +fn convert_vap_to_cluster_admission_policy( + cel_policy_module: &str, + vap: ValidatingAdmissionPolicy, + vap_binding: ValidatingAdmissionPolicyBinding, +) -> anyhow::Result { + let vap_spec = vap.spec.unwrap_or_default(); + if vap_spec.audit_annotations.is_some() { + warn!("auditAnnotations are not supported by Kubewarden's CEL policy yet. They will be ignored."); + } + if vap_spec.match_conditions.is_some() { + warn!("matchConditions are not supported by Kubewarden's CEL policy yet. They will be ignored."); + } + if vap_spec.param_kind.is_some() { + // It's not safe to skip this, the policy will definitely not work. + return Err(anyhow!( + "paramKind is not supported by Kubewarden's CEL policy yet" + )); + } + + let mut settings = serde_yaml::Mapping::new(); + + // migrate CEL variables + if let Some(vap_variables) = vap_spec.variables { + let vap_variables: Vec = vap_variables + .iter() + .map(|v| serde_yaml::to_value(v).expect("cannot convert VAP variable to YAML")) + .collect(); + settings.insert("variables".into(), vap_variables.into()); + } + + // migrate CEL validations + if let Some(vap_validations) = vap_spec.validations { + let kw_cel_validations: Vec = vap_validations + .iter() + .map(|v| serde_yaml::to_value(v).expect("cannot convert VAP validation to YAML")) + .collect(); + settings.insert("validations".into(), kw_cel_validations.into()); + } + + // VAP specifies the namespace selector inside of the binding + let namespace_selector = vap_binding + .spec + .unwrap_or_default() + .match_resources + .unwrap_or_default() + .namespace_selector; + + // VAP rules are specified inside of the VAP object + let vap_match_constraints = vap_spec.match_constraints.unwrap_or_default(); + let match_policy = vap_match_constraints.match_policy; + let rules = vap_match_constraints + .resource_rules + .unwrap_or_default() + .iter() + .map(Rule::try_from) + .collect::, &'static str>>() + .map_err(|e| anyhow!("error converting VAP matchConstraints into rules: {e}"))?; + + // migrate VAP + let cluster_admission_policy = ClusterAdmissionPolicy { + api_version: "policies.kubewarden.io/v1".to_string(), + kind: "ClusterAdmissionPolicy".to_string(), + metadata: vap_binding.metadata, + spec: ClusterAdmissionPolicySpec { + module: cel_policy_module.to_string(), + namespace_selector, + match_policy, + rules, + object_selector: vap_match_constraints.object_selector, + mutating: false, + background_audit: true, + context_aware_resources: BTreeSet::new(), + failure_policy: vap_spec.failure_policy, + mode: None, // VAP policies are always in protect mode, which is the default for KW + settings, + }, + }; + + Ok(cluster_admission_policy) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + + const CEL_POLICY_MODULE: &str = "ghcr.io/kubewarden/policies/cel-policy:latest"; + + fn test_data(path: &str) -> String { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("data") + .join(path) + .to_string_lossy() + .to_string() + } + + #[rstest] + #[case::vap_without_variables("vap/vap-without-variables.yml", "vap/vap-binding.yml", false)] + #[case::vap_with_variables("vap/vap-with-variables.yml", "vap/vap-binding.yml", true)] + fn from_vap_to_cluster_admission_policy( + #[case] vap_yaml_path: &str, + #[case] vap_binding_yaml_path: &str, + #[case] has_variables: bool, + ) { + let yaml_file = File::open(test_data(vap_yaml_path)).unwrap(); + let vap: ValidatingAdmissionPolicy = serde_yaml::from_reader(yaml_file).unwrap(); + + let expected_validations = + serde_yaml::to_value(vap.clone().spec.unwrap().validations.unwrap()).unwrap(); + let expected_rules = vap + .clone() + .spec + .unwrap() + .match_constraints + .unwrap() + .resource_rules + .unwrap() + .iter() + .map(Rule::try_from) + .collect::, &str>>() + .unwrap(); + + let yaml_file = File::open(test_data(vap_binding_yaml_path)).unwrap(); + let vap_binding: ValidatingAdmissionPolicyBinding = + serde_yaml::from_reader(yaml_file).unwrap(); + + let cluster_admission_policy = convert_vap_to_cluster_admission_policy( + CEL_POLICY_MODULE, + vap.clone(), + vap_binding.clone(), + ) + .unwrap(); + + assert_eq!(CEL_POLICY_MODULE, cluster_admission_policy.spec.module); + assert!(!cluster_admission_policy.spec.mutating); + assert_eq!(cluster_admission_policy.spec.rules, expected_rules); + assert!(cluster_admission_policy.spec.background_audit); + assert!(cluster_admission_policy + .spec + .context_aware_resources + .is_empty()); + assert_eq!( + vap.clone().spec.unwrap().failure_policy, + cluster_admission_policy.spec.failure_policy + ); + assert!(cluster_admission_policy.spec.mode.is_none()); + assert_eq!( + vap.clone() + .spec + .unwrap() + .match_constraints + .unwrap() + .match_policy, + cluster_admission_policy.spec.match_policy + ); + assert_eq!( + vap_binding + .clone() + .spec + .unwrap() + .match_resources + .unwrap() + .namespace_selector, + cluster_admission_policy.spec.namespace_selector + ); + assert!(cluster_admission_policy.spec.object_selector.is_none()); + assert_eq!( + expected_validations, + cluster_admission_policy.spec.settings["validations"] + ); + + if has_variables { + let expected_variables = + serde_yaml::to_value(vap.clone().spec.unwrap().variables.unwrap()).unwrap(); + assert_eq!( + expected_variables, + cluster_admission_policy.spec.settings["variables"] + ); + } else { + assert!(!cluster_admission_policy + .spec + .settings + .contains_key("variables")); + } + } +} diff --git a/src/scaffold/verification_config.rs b/src/scaffold/verification_config.rs new file mode 100644 index 00000000..93ee605b --- /dev/null +++ b/src/scaffold/verification_config.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use policy_evaluator::policy_fetcher::verify::config::{ + LatestVerificationConfig, Signature, VersionedVerificationConfig, +}; + +pub(crate) fn verification_config() -> Result { + let mut comment_header = r#"# Default Kubewarden verification config +# +# With this config, the only valid policies are those signed by Kubewarden +# infrastructure. +# +# This config can be saved to its default location (for this OS) with: +# kwctl scaffold verification-config > "# + .to_string(); + + comment_header.push_str( + crate::KWCTL_DEFAULT_VERIFICATION_CONFIG_PATH + .to_owned() + .as_str(), + ); + comment_header.push_str( + r#" +# +# Providing a config in the default location enables Sigstore verification. +# See https://docs.kubewarden.io for more Sigstore verification options."#, + ); + + let kubewarden_verification_config = + VersionedVerificationConfig::V1(LatestVerificationConfig { + all_of: Some(vec![Signature::GithubAction { + owner: "kubewarden".to_string(), + repo: None, + annotations: None, + }]), + any_of: None, + }); + + Ok(format!( + "{}\n{}", + comment_header, + serde_yaml::to_string(&kubewarden_verification_config)? + )) +} From d39250fc2c9493fa082f56bd67189d4a4a5bafa4 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Wed, 31 Jul 2024 18:22:06 +0200 Subject: [PATCH 2/2] fix: update code to build with Rust 1.80 Starting from Rust 1.80, setting environment variables from Rust code is now `unsafe`. Signed-off-by: Flavio Castelli --- src/main.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 301aae62..641d1fc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,9 +98,13 @@ async fn main() -> Result<()> { // adapt the output. This can later be removed if // prettytable provides methods to disable color globally if no_color { - env::set_var("TERM", "dumb"); + unsafe { + env::set_var("TERM", "dumb"); + } } else { - env::set_var("TERM", term_color_support); + unsafe { + env::set_var("TERM", term_color_support); + } } // setup logging @@ -473,7 +477,9 @@ fn remote_server_options(matches: &ArgMatches) -> Result> { if let Some(docker_config_json_path) = matches.get_one::("docker-config-json-path") { // docker_credential crate expects the config path in the $DOCKER_CONFIG. Keep docker-config-json-path parameter for backwards compatibility - env::set_var(DOCKER_CONFIG_ENV_VAR, docker_config_json_path); + unsafe { + env::set_var(DOCKER_CONFIG_ENV_VAR, docker_config_json_path); + } } if let Ok(docker_config_path_str) = env::var(DOCKER_CONFIG_ENV_VAR) { let docker_config_path = Path::new(&docker_config_path_str).join("config.json");