diff --git a/src/admission_request.rs b/src/admission_request.rs new file mode 100644 index 00000000..a5d0a9e0 --- /dev/null +++ b/src/admission_request.rs @@ -0,0 +1,44 @@ +/// This models the admission/v1/AdmissionRequest object of Kubernetes +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdmissionRequest { + pub uid: String, + pub kind: GroupVersionKind, + pub resource: GroupVersionResource, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub_resource: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_resource: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_sub_resource: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace: Option, + pub operation: String, + pub user_info: k8s_openapi::api::authentication::v1::UserInfo, + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub old_object: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dry_run: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct GroupVersionKind { + pub group: String, + pub version: String, + pub kind: String, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct GroupVersionResource { + pub group: String, + pub version: String, + pub resource: String, +} diff --git a/src/admission_response.rs b/src/admission_response.rs index 47566c48..005d5a6d 100644 --- a/src/admission_response.rs +++ b/src/admission_response.rs @@ -147,7 +147,7 @@ mod tests { let response = AdmissionResponse::reject(uid.clone(), message.clone(), code); assert_eq!(response.uid, uid); - assert_eq!(response.allowed, false); + assert!(!response.allowed); assert_eq!(response.patch, None); assert_eq!(response.patch_type, None); @@ -187,7 +187,7 @@ mod tests { let response = response.unwrap(); assert_eq!(response.uid, uid); - assert_eq!(response.allowed, false); + assert!(!response.allowed); assert_eq!(response.patch, None); assert_eq!(response.patch_type, None); assert_eq!(response.audit_annotations, Some(audit_annotations)); diff --git a/src/lib.rs b/src/lib.rs index 1170b77f..820eb255 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub extern crate burrego; extern crate wasmparser; +pub mod admission_request; pub mod admission_response; pub mod callback_handler; pub mod callback_requests; diff --git a/src/policy.rs b/src/policy.rs index 8a85d476..8fe00b39 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -13,7 +13,7 @@ use crate::policy_metadata::ContextAwareResource; /// This struct is used extensively inside of the `host_callback` /// function to obtain information about the policy that is invoking /// a host waPC function, and handle the request. -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Policy { /// The policy identifier. This is mostly relevant for Policy Server, /// which uses the identifier provided by the user inside of the `policy.yml` @@ -56,18 +56,6 @@ impl PartialEq for Policy { } } -#[cfg(test)] -impl Default for Policy { - fn default() -> Self { - Policy { - id: String::default(), - instance_id: None, - callback_channel: None, - ctx_aware_resources_allow_list: HashSet::new(), - } - } -} - impl Policy { pub(crate) fn new( id: String, @@ -167,18 +155,18 @@ mod tests { let id = "test".to_string(); let policy_id = Some(1); let callback_channel = None; - let ctx_aware_resources_allow_list = Some(HashSet::new()); + let ctx_aware_resources_allow_list = HashSet::new(); let policy = Policy::new( id.clone(), - policy_id.clone(), + policy_id, callback_channel.clone(), - ctx_aware_resources_allow_list.clone(), + Some(ctx_aware_resources_allow_list.clone()), ) .expect("cannot create policy"); assert!(policy.id == id); assert!(policy.instance_id == policy_id); - assert!(policy.ctx_aware_resources_allow_list == ctx_aware_resources_allow_list.unwrap()); + assert!(policy.ctx_aware_resources_allow_list == ctx_aware_resources_allow_list); } } diff --git a/src/policy_evaluator.rs b/src/policy_evaluator.rs index 090ae23d..5ba00d65 100644 --- a/src/policy_evaluator.rs +++ b/src/policy_evaluator.rs @@ -5,6 +5,7 @@ use serde::Serialize; use serde_json::value; use std::{convert::TryFrom, fmt}; +use crate::admission_request::AdmissionRequest; use crate::admission_response::AdmissionResponse; use crate::policy::Policy; use crate::runtimes::burrego::Runtime as BurregoRuntime; @@ -32,19 +33,23 @@ impl fmt::Display for PolicyExecutionMode { } } -#[derive(Debug, Serialize)] -pub struct ValidateRequest(pub(crate) serde_json::Value); +/// A validation request that can be sent to a policy evaluator. +/// It can be either a raw JSON object, or a Kubernetes AdmissionRequest. +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum ValidateRequest { + Raw(serde_json::Value), + AdmissionRequest(AdmissionRequest), +} impl ValidateRequest { - pub fn new(request: serde_json::Value) -> Self { - ValidateRequest(request) - } - pub(crate) fn uid(&self) -> &str { - if let Some(uid) = self.0.get("uid").and_then(value::Value::as_str) { - uid - } else { - "" + match self { + ValidateRequest::Raw(raw_req) => raw_req + .get("uid") + .and_then(value::Value::as_str) + .unwrap_or_default(), + ValidateRequest::AdmissionRequest(adm_req) => &adm_req.uid, } } } @@ -196,7 +201,7 @@ mod tests { for (mode_str, expected) in &test_data { let actual: std::result::Result = - serde_json::from_str(&mode_str); + serde_json::from_str(mode_str); assert_eq!(expected, &actual.unwrap()); } diff --git a/src/policy_metadata.rs b/src/policy_metadata.rs index b2b22985..a0950110 100644 --- a/src/policy_metadata.rs +++ b/src/policy_metadata.rs @@ -319,7 +319,7 @@ mod tests { #[test] fn metadata_with_rego_execution_mode_must_have_a_valid_protocol() { - for mode in vec![PolicyExecutionMode::Opa, PolicyExecutionMode::OpaGatekeeper] { + for mode in [PolicyExecutionMode::Opa, PolicyExecutionMode::OpaGatekeeper] { let metadata = Metadata { protocol_version: Some(ProtocolVersion::Unknown), execution_mode: mode, diff --git a/src/runtimes/burrego.rs b/src/runtimes/burrego.rs index 97b08c8b..19934068 100644 --- a/src/runtimes/burrego.rs +++ b/src/runtimes/burrego.rs @@ -40,20 +40,24 @@ impl<'a> Runtime<'a> { // OPA and Gatekeeper expect arguments in different ways. Provide the ones that each expect. let (document_to_evaluate, data) = match self.0.policy_execution_mode { - RegoPolicyExecutionMode::Opa => { - // Policies for OPA expect the whole `AdmissionReview` - // object: produce a synthetic external one so - // existing OPA policies are compatible. - ( - json!({ - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "request": &request.0, - }), - json!(settings), - ) - } + RegoPolicyExecutionMode::Opa => ( + json!({ + "request": &request, + }), + json!(settings), + ), RegoPolicyExecutionMode::Gatekeeper => { + // Gatekeeper policies expect the `AdmissionRequest` variant only. + let request = match request { + ValidateRequest::AdmissionRequest(adm_req) => adm_req, + ValidateRequest::Raw(_) => { + return AdmissionResponse::reject_internal_server_error( + uid.to_string(), + "Gatekeeper does not support raw validation requests".to_string(), + ); + } + }; + // Gatekeeper policies include a toplevel `review` // object that contains the AdmissionRequest to be // evaluated in an `object` attribute, and the @@ -62,7 +66,7 @@ impl<'a> Runtime<'a> { ( json!({ "parameters": settings, - "review": &request.0, + "review": &request, }), json!({"kubernetes": ""}), // TODO (ereslibre): Kubernetes context goes here ) diff --git a/src/runtimes/wapc.rs b/src/runtimes/wapc.rs index b11761a5..aa2ca0e2 100644 --- a/src/runtimes/wapc.rs +++ b/src/runtimes/wapc.rs @@ -513,8 +513,14 @@ impl<'a> Runtime<'a> { ) -> AdmissionResponse { let uid = request.uid(); + let req_json_value = + serde_json::to_value(request).expect("cannot convert request to json value"); + //NOTE: object is null for DELETE operations - let req_obj = request.0.get("object"); + let req_obj = match request { + ValidateRequest::Raw(_) => Some(&req_json_value), + ValidateRequest::AdmissionRequest(_) => req_json_value.get("object"), + }; let validate_params = json!({ "request": request, diff --git a/src/runtimes/wasi_cli/runtime.rs b/src/runtimes/wasi_cli/runtime.rs index a6568340..4cbf3be2 100644 --- a/src/runtimes/wasi_cli/runtime.rs +++ b/src/runtimes/wasi_cli/runtime.rs @@ -119,11 +119,20 @@ impl<'a> Runtime<'a> { ) } match serde_json::from_slice::(stdout.as_bytes()) { - Ok(pvr) => AdmissionResponse::from_policy_validation_response( - request.uid().to_string(), - request.0.get("object"), - &pvr, - ) + Ok(pvr) => { + let req_json_value = serde_json::to_value(request) + .expect("cannot convert request to json value"); + let req_obj = match request { + ValidateRequest::Raw(_) => Some(&req_json_value), + ValidateRequest::AdmissionRequest(_) => req_json_value.get("object"), + }; + + AdmissionResponse::from_policy_validation_response( + request.uid().to_string(), + req_obj, + &pvr, + ) + } .unwrap_or_else(|e| { AdmissionResponse::reject_internal_server_error( request.uid().to_string(),