Skip to content

Commit

Permalink
Merge pull request #68 from kubewarden/opa
Browse files Browse the repository at this point in the history
Enable evaluation of Rego-based policies
  • Loading branch information
flavio authored Sep 20, 2021
2 parents cb05703 + 96c38bb commit 944f14f
Show file tree
Hide file tree
Showing 10 changed files with 1,094 additions and 368 deletions.
674 changes: 371 additions & 303 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "kwctl"
description = "Tool to manage Kubewarden policies"
version = "0.1.10"
version = "0.2.0"
authors = [
"Flavio Castelli <[email protected]>",
"Rafael Fernández López <[email protected]>",
Expand All @@ -14,11 +14,13 @@ edition = "2018"
anyhow = "1.0"
clap = "2.33.3"
directories = "3.0.2"
itertools = "0.10.1"
k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] }
kube = "0.51.0"
kubewarden-policy-sdk = "0.2.3"
lazy_static = "1.4.0"
mdcat = "0.22"
policy-evaluator = { git = "https://github.com/kubewarden/policy-evaluator", tag = "v0.1.19" }
policy-evaluator = { git = "https://github.com/kubewarden/policy-evaluator", tag = "v0.2.0" }
policy-fetcher = { git = "https://github.com/kubewarden/policy-fetcher", tag = "v0.1.13" }
pretty-bytes = "0.2.2"
prettytable-rs = "^0.8"
Expand All @@ -37,6 +39,7 @@ tracing-subscriber = { version= "0.2", features = ["fmt"] }
url = "2.2.0"
validator = { version = "0.13", features = ["derive"] }
walrus = "0.19.0"
wasmparser = "0.80.0"

[dev-dependencies]
tempfile = "3.2.0"
107 changes: 82 additions & 25 deletions src/annotate.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::backend::{Backend, BackendDetector};
use anyhow::{anyhow, Result};
use kubewarden_policy_sdk::metadata::ProtocolVersion;
use policy_evaluator::{
constants::*, policy_evaluator::PolicyEvaluator, policy_metadata::Metadata,
};
use policy_evaluator::{constants::*, policy_metadata::Metadata};
use std::fs::File;
use std::path::PathBuf;
use validator::Validate;
Expand All @@ -12,32 +11,30 @@ pub(crate) fn write_annotation(
metadata_path: PathBuf,
destination: PathBuf,
) -> Result<()> {
let metadata = prepare_metadata(wasm_path.clone(), metadata_path, protocol_detector)?;
let backend_detector = BackendDetector::default();
let metadata = prepare_metadata(wasm_path.clone(), metadata_path, backend_detector)?;
write_annotated_wasm_file(wasm_path, destination, metadata)
}

fn protocol_detector(wasm_path: PathBuf) -> Result<ProtocolVersion> {
let policy_evaluator = PolicyEvaluator::from_file(
String::from(wasm_path.to_string_lossy()),
wasm_path.as_path(),
None,
)?;
policy_evaluator
.protocol_version()
.map_err(|e| anyhow!("Cannot compute ProtocolVersion used by the policy: {:?}", e))
}

fn prepare_metadata(
wasm_path: PathBuf,
metadata_path: PathBuf,
detect_protocol_func: impl Fn(PathBuf) -> Result<ProtocolVersion>,
backend_detector: BackendDetector,
) -> Result<Metadata> {
let metadata_file = File::open(metadata_path)?;
let mut metadata: Metadata = serde_yaml::from_reader(&metadata_file)?;

let protocol_version = detect_protocol_func(wasm_path)?;

metadata.protocol_version = Some(protocol_version);
let metadata_file =
File::open(metadata_path).map_err(|e| anyhow!("Error opening metadata file: {}", e))?;
let mut metadata: Metadata = serde_yaml::from_reader(&metadata_file)
.map_err(|e| anyhow!("Error unmarshalling metadata {}", e))?;

let backend = backend_detector.detect(wasm_path, &metadata)?;

match backend {
Backend::Opa => metadata.protocol_version = Some(ProtocolVersion::Unknown),
Backend::OpaGatekeeper => metadata.protocol_version = Some(ProtocolVersion::Unknown),
Backend::KubewardenWapc(protocol_version) => {
metadata.protocol_version = Some(protocol_version)
}
};

let mut annotations = metadata.annotations.unwrap_or_default();
annotations.insert(
Expand Down Expand Up @@ -82,6 +79,14 @@ mod tests {
Ok(ProtocolVersion::V1)
}

fn mock_rego_policy_detector_true(_wasm_path: PathBuf) -> Result<bool> {
Ok(true)
}

fn mock_rego_policy_detector_false(_wasm_path: PathBuf) -> Result<bool> {
Ok(false)
}

#[test]
fn test_kwctl_version_is_added_to_already_populated_annotations() -> Result<()> {
let dir = tempdir()?;
Expand All @@ -106,10 +111,14 @@ mod tests {

write!(file, "{}", raw_metadata)?;

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_false,
mock_protocol_version_detector_v1,
);
let metadata = prepare_metadata(
PathBuf::from("irrelevant.wasm"),
file_path,
mock_protocol_version_detector_v1,
backend_detector,
)?;
let annotations = metadata.annotations.unwrap();

Expand Down Expand Up @@ -151,10 +160,14 @@ mod tests {

write!(file, "{}", raw_metadata)?;

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_false,
mock_protocol_version_detector_v1,
);
let metadata = prepare_metadata(
PathBuf::from("irrelevant.wasm"),
file_path,
mock_protocol_version_detector_v1,
backend_detector,
)?;
let annotations = metadata.annotations.unwrap();

Expand Down Expand Up @@ -186,15 +199,20 @@ mod tests {
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
mutating: false
executionMode: kubewarden-wapc
"#
);

write!(file, "{}", raw_metadata)?;

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_false,
mock_protocol_version_detector_v1,
);
let metadata = prepare_metadata(
PathBuf::from("irrelevant.wasm"),
file_path,
mock_protocol_version_detector_v1,
backend_detector,
)?;
let annotations = metadata.annotations.unwrap();

Expand All @@ -205,4 +223,43 @@ mod tests {

Ok(())
}

#[test]
fn test_final_metadata_for_a_rego_policy() -> Result<()> {
let dir = tempdir()?;

let file_path = dir.path().join("metadata.yml");
let mut file = File::create(file_path.clone())?;

let raw_metadata = String::from(
r#"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
mutating: false
executionMode: opa
"#,
);

write!(file, "{}", raw_metadata)?;

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_true,
mock_protocol_version_detector_v1,
);
let metadata = prepare_metadata(
PathBuf::from("irrelevant.wasm"),
file_path,
backend_detector,
);
assert!(metadata.is_ok());
assert_eq!(
metadata.unwrap().protocol_version,
Some(ProtocolVersion::Unknown)
);

Ok(())
}
}
172 changes: 172 additions & 0 deletions src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use anyhow::{anyhow, Result};
use kubewarden_policy_sdk::metadata::ProtocolVersion;
use policy_evaluator::{
policy_evaluator::{PolicyEvaluator, PolicyExecutionMode},
policy_metadata::Metadata,
};
use std::path::{Path, PathBuf};

pub(crate) enum Backend {
Opa,
OpaGatekeeper,
KubewardenWapc(ProtocolVersion),
}

type KubewardenProtocolDetectorFn = fn(PathBuf) -> Result<ProtocolVersion>;
type RegoDetectorFn = fn(PathBuf) -> Result<bool>;

// Looks at the Wasm module pointed by `wasm_path` and return whether it was generaed by a Rego
// policy
//
// The code looks at the export symbols offered by the Wasm module.
// Having at least one symbol that starts with the `opa_` prefix leads
// the policy to be considered a Rego-based one.
fn rego_policy_detector(wasm_path: PathBuf) -> Result<bool> {
let data: Vec<u8> = std::fs::read(wasm_path)?;
for payload in wasmparser::Parser::new(0).parse_all(&data) {
if let wasmparser::Payload::ExportSection(s) = payload? {
for export in s {
if export?.field.starts_with("opa_") {
return Ok(true);
}
}
}
}

Ok(false)
}

fn kubewarden_protocol_detector(wasm_path: PathBuf) -> Result<ProtocolVersion> {
let mut policy_evaluator = PolicyEvaluator::from_file(
String::from(wasm_path.to_string_lossy()),
wasm_path.as_path(),
PolicyExecutionMode::KubewardenWapc,
None,
)?;
policy_evaluator
.protocol_version()
.map_err(|e| anyhow!("Cannot compute ProtocolVersion used by the policy: {:?}", e))
}

pub(crate) struct BackendDetector {
kubewarden_protocol_detector_func: KubewardenProtocolDetectorFn,
rego_detector_func: RegoDetectorFn,
}

impl Default for BackendDetector {
fn default() -> Self {
BackendDetector {
kubewarden_protocol_detector_func: kubewarden_protocol_detector,
rego_detector_func: rego_policy_detector,
}
}
}

impl BackendDetector {
#[allow(dead_code)]
/// This method is intended to be used by unit tests
pub(crate) fn new(
rego_detector_func: RegoDetectorFn,
kubewarden_protocol_detector_func: KubewardenProtocolDetectorFn,
) -> Self {
BackendDetector {
kubewarden_protocol_detector_func,
rego_detector_func,
}
}

pub(crate) fn is_rego_policy(&self, wasm_path: &Path) -> Result<bool> {
(self.rego_detector_func)(wasm_path.to_path_buf()).map_err(|e| {
anyhow!(
"Error while checking if the policy has been created using Opa/Gatekeeper: {}",
e
)
})
}

pub(crate) fn detect(&self, wasm_path: PathBuf, metadata: &Metadata) -> Result<Backend> {
let is_rego_policy = self.is_rego_policy(&wasm_path)?;
match metadata.execution_mode {
PolicyExecutionMode::Opa => {
if is_rego_policy {
Ok(Backend::Opa)
} else {
Err(anyhow!(
"Wrong value inside of policy's metatada for 'executionMode'. The policy has not been created using Rego"
))
}
}
PolicyExecutionMode::OpaGatekeeper => {
if is_rego_policy {
Ok(Backend::OpaGatekeeper)
} else {
Err(anyhow!(
"Wrong value inside of policy's metatada for 'executionMode'. The policy has not been created using Rego"
))
}
}
PolicyExecutionMode::KubewardenWapc => {
if is_rego_policy {
Err(anyhow!(
"Wrong value inside of policy's metatada for 'executionMode'. This policy has been created using Rego"
))
} else {
let protocol_version = (self.kubewarden_protocol_detector_func)(wasm_path)
.map_err(|e| {
anyhow!("Error while detecting Kubewarden protocol version: {:?}", e)
})?;
Ok(Backend::KubewardenWapc(protocol_version))
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn mock_protocol_version_detector_v1(_wasm_path: PathBuf) -> Result<ProtocolVersion> {
Ok(ProtocolVersion::V1)
}

fn mock_rego_policy_detector_true(_wasm_path: PathBuf) -> Result<bool> {
Ok(true)
}

fn mock_rego_policy_detector_false(_wasm_path: PathBuf) -> Result<bool> {
Ok(false)
}

#[test]
fn test_execution_mode_cannot_be_kubewarden_for_a_rego_policy() {
let metadata = Metadata {
execution_mode: PolicyExecutionMode::KubewardenWapc,
..Default::default()
};

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_true,
mock_protocol_version_detector_v1,
);
let backend = backend_detector.detect(PathBuf::from("irrelevant.wasm"), &metadata);
assert!(backend.is_err());
}

#[test]
fn test_execution_mode_cannot_be_opa_or_gatekeeper_for_a_kubewarden_policy() {
for execution_mode in vec![PolicyExecutionMode::Opa, PolicyExecutionMode::OpaGatekeeper] {
let metadata = Metadata {
execution_mode,
..Default::default()
};

let backend_detector = BackendDetector::new(
mock_rego_policy_detector_false,
mock_protocol_version_detector_v1,
);
let backend = backend_detector.detect(PathBuf::from("irrelevant.wasm"), &metadata);
assert!(backend.is_err());
}
}
}
Loading

0 comments on commit 944f14f

Please sign in to comment.