Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: raw policy validation #357

Merged
merged 6 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/admission_request.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_kind: Option<GroupVersionKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_resource: Option<GroupVersionResource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_sub_resource: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
pub operation: String,
pub user_info: k8s_openapi::api::authentication::v1::UserInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<k8s_openapi::apimachinery::pkg::runtime::RawExtension>,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_object: Option<k8s_openapi::apimachinery::pkg::runtime::RawExtension>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dry_run: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<k8s_openapi::apimachinery::pkg::runtime::RawExtension>,
}

#[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,
}
4 changes: 2 additions & 2 deletions src/admission_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
22 changes: 5 additions & 17 deletions src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
27 changes: 16 additions & 11 deletions src/policy_evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -196,7 +201,7 @@ mod tests {

for (mode_str, expected) in &test_data {
let actual: std::result::Result<PolicyExecutionMode, serde_json::Error> =
serde_json::from_str(&mode_str);
serde_json::from_str(mode_str);
assert_eq!(expected, &actual.unwrap());
}

Expand Down
2 changes: 1 addition & 1 deletion src/policy_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 18 additions & 14 deletions src/runtimes/burrego.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -62,7 +66,7 @@ impl<'a> Runtime<'a> {
(
json!({
"parameters": settings,
"review": &request.0,
"review": &request,
}),
json!({"kubernetes": ""}), // TODO (ereslibre): Kubernetes context goes here
)
Expand Down
8 changes: 7 additions & 1 deletion src/runtimes/wapc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 14 additions & 5 deletions src/runtimes/wasi_cli/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,20 @@ impl<'a> Runtime<'a> {
)
}
match serde_json::from_slice::<PolicyValidationResponse>(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(),
Expand Down
Loading