Skip to content

Commit

Permalink
Merge pull request #161 from Kuadrant/proposal-refactor
Browse files Browse the repository at this point in the history
Refactor entire filter flow
  • Loading branch information
adam-cattermole authored Jan 28, 2025
2 parents a4a0533 + bc55c1c commit ff60de9
Show file tree
Hide file tree
Showing 29 changed files with 1,303 additions and 1,394 deletions.
233 changes: 220 additions & 13 deletions src/auth_action.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::configuration::{Action, FailureMode, Service};
use crate::data::Predicate;
use crate::service::GrpcService;
use log::error;
use crate::data::{store_metadata, Predicate, PredicateVec};
use crate::envoy::{CheckResponse, CheckResponse_oneof_http_response, HeaderValueOption};
use crate::service::{GrpcErrResponse, GrpcService, Headers};
use log::debug;
use std::rc::Rc;

#[derive(Debug)]
Expand Down Expand Up @@ -34,28 +35,89 @@ impl AuthAction {
}

pub fn conditions_apply(&self) -> bool {
let predicates = &self.predicates;
predicates.is_empty()
|| predicates.iter().all(|predicate| match predicate.test() {
Ok(b) => b,
Err(err) => {
error!("Failed to evaluate {:?}: {}", predicate, err);
panic!("Err out of this!")
}
})
self.predicates.apply()
}

pub fn get_failure_mode(&self) -> FailureMode {
self.grpc_service.get_failure_mode()
}

pub fn process_response(
&self,
check_response: CheckResponse,
) -> Result<Headers, GrpcErrResponse> {
//todo(adam-cattermole):hostvar resolver?
// store dynamic metadata in filter state
debug!("process_response(auth): store_metadata");
store_metadata(check_response.get_dynamic_metadata());

match check_response.http_response {
None => {
debug!("process_response(auth): received no http_response");
match self.get_failure_mode() {
FailureMode::Deny => Err(GrpcErrResponse::new_internal_server_error()),
FailureMode::Allow => {
debug!("process_response(auth): continuing as FailureMode Allow");
Ok(Vec::default())
}
}
}
Some(CheckResponse_oneof_http_response::denied_response(denied_response)) => {
debug!("process_response(auth): received DeniedHttpResponse");
let status_code = denied_response.get_status().get_code();
let response_headers = Self::get_header_vec(denied_response.get_headers());
Err(GrpcErrResponse::new(
status_code as u32,
response_headers,
denied_response.body,
))
}
Some(CheckResponse_oneof_http_response::ok_response(ok_response)) => {
debug!("process_response(auth): received OkHttpResponse");

if !ok_response.get_response_headers_to_add().is_empty() {
panic!("process_response(auth): response contained response_headers_to_add which is unsupported!")
}
if !ok_response.get_headers_to_remove().is_empty() {
panic!("process_response(auth): response contained headers_to_remove which is unsupported!")
}
if !ok_response.get_query_parameters_to_set().is_empty() {
panic!("process_response(auth): response contained query_parameters_to_set which is unsupported!")
}
if !ok_response.get_query_parameters_to_remove().is_empty() {
panic!("process_response(auth): response contained query_parameters_to_remove which is unsupported!")
}
Ok(Self::get_header_vec(ok_response.get_headers()))
}
}
}

fn get_header_vec(headers: &[HeaderValueOption]) -> Headers {
headers
.iter()
.map(|header| {
let hv = header.get_header();
(hv.key.to_owned(), hv.value.to_owned())
})
.collect()
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::configuration::{Action, FailureMode, Service, ServiceType, Timeout};
use crate::envoy::{DeniedHttpResponse, HeaderValue, HttpStatus, OkHttpResponse, StatusCode};
use protobuf::RepeatedField;

fn build_auth_action_with_predicates(predicates: Vec<String>) -> AuthAction {
build_auth_action_with_predicates_and_failure_mode(predicates, FailureMode::default())
}

fn build_auth_action_with_predicates_and_failure_mode(
predicates: Vec<String>,
failure_mode: FailureMode,
) -> AuthAction {
let action = Action {
service: "some_service".into(),
scope: "some_scope".into(),
Expand All @@ -66,14 +128,64 @@ mod test {
let service = Service {
service_type: ServiceType::Auth,
endpoint: "some_endpoint".into(),
failure_mode: FailureMode::default(),
failure_mode,
timeout: Timeout::default(),
};

AuthAction::new(&action, &service)
.expect("action building failed. Maybe predicates compilation?")
}

fn build_check_response(
status: StatusCode,
headers: Option<Vec<(&str, &str)>>,
body: Option<String>,
) -> CheckResponse {
let mut response = CheckResponse::new();
match status {
StatusCode::OK => {
let mut ok_http_response = OkHttpResponse::new();
if let Some(header_list) = headers {
ok_http_response.set_headers(build_headers(header_list))
}
response.set_ok_response(ok_http_response);
}
StatusCode::Forbidden => {
let mut http_status = HttpStatus::new();
http_status.set_code(status);

let mut denied_http_response = DeniedHttpResponse::new();
denied_http_response.set_status(http_status);
if let Some(header_list) = headers {
denied_http_response.set_headers(build_headers(header_list));
}
denied_http_response.set_body(body.unwrap_or_default());
response.set_denied_response(denied_http_response);
}
_ => {
// assume any other code is for error state
}
};
response
}

fn build_headers(headers: Vec<(&str, &str)>) -> RepeatedField<HeaderValueOption> {
headers
.into_iter()
.map(|(key, value)| {
let header_value = {
let mut hv = HeaderValue::new();
hv.set_key(key.to_string());
hv.set_value(value.to_string());
hv
};
let mut header_option = HeaderValueOption::new();
header_option.set_header(header_value);
header_option
})
.collect::<RepeatedField<HeaderValueOption>>()
}

#[test]
fn empty_predicates_do_apply() {
let auth_action = build_auth_action_with_predicates(Vec::default());
Expand Down Expand Up @@ -108,4 +220,99 @@ mod test {
]);
auth_action.conditions_apply();
}

#[test]
fn process_ok_response() {
let auth_action = build_auth_action_with_predicates(Vec::default());
let ok_response_without_headers = build_check_response(StatusCode::OK, None, None);
let result = auth_action.process_response(ok_response_without_headers);
assert!(result.is_ok());

let headers = result.expect("is ok");
assert!(headers.is_empty());

let ok_response_with_header =
build_check_response(StatusCode::OK, Some(vec![("my_header", "my_value")]), None);
let result = auth_action.process_response(ok_response_with_header);
assert!(result.is_ok());

let headers = result.expect("is ok");
assert!(!headers.is_empty());

assert_eq!(
headers[0],
("my_header".to_string(), "my_value".to_string())
);
}

#[test]
fn process_denied_response() {
let headers = vec![
("www-authenticate", "APIKEY realm=\"api-key-users\""),
("x-ext-auth-reason", "credential not found"),
];
let auth_action = build_auth_action_with_predicates(Vec::default());
let denied_response_empty = build_check_response(StatusCode::Forbidden, None, None);
let result = auth_action.process_response(denied_response_empty);
assert!(result.is_err());

let grpc_err_response = result.expect_err("is err");
assert_eq!(
grpc_err_response.status_code(),
StatusCode::Forbidden as u32
);
assert!(grpc_err_response.headers().is_empty());
assert_eq!(grpc_err_response.body(), String::default());

let denied_response_content = build_check_response(
StatusCode::Forbidden,
Some(headers.clone()),
Some("my_body".to_string()),
);
let result = auth_action.process_response(denied_response_content);
assert!(result.is_err());

let grpc_err_response = result.expect_err("is err");
assert_eq!(
grpc_err_response.status_code(),
StatusCode::Forbidden as u32
);

let response_headers = grpc_err_response.headers();
headers.iter().zip(response_headers.iter()).for_each(
|((header_one, value_one), (header_two, value_two))| {
assert_eq!(header_one, header_two);
assert_eq!(value_one, value_two);
},
);

assert_eq!(grpc_err_response.body(), "my_body");
}

#[test]
fn process_error_response() {
let auth_action =
build_auth_action_with_predicates_and_failure_mode(Vec::default(), FailureMode::Deny);
let error_response = build_check_response(StatusCode::InternalServerError, None, None);
let result = auth_action.process_response(error_response);
assert!(result.is_err());

let grpc_err_response = result.expect_err("is err");
assert_eq!(
grpc_err_response.status_code(),
StatusCode::InternalServerError as u32
);

assert!(grpc_err_response.headers().is_empty());
assert_eq!(grpc_err_response.body(), "Internal Server Error.\n");

let auth_action =
build_auth_action_with_predicates_and_failure_mode(Vec::default(), FailureMode::Allow);
let error_response = build_check_response(StatusCode::InternalServerError, None, None);
let result = auth_action.process_response(error_response);
assert!(result.is_ok());

let headers = result.expect("is ok");
assert!(headers.is_empty());
}
}
3 changes: 1 addition & 2 deletions src/data/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::data::PropertyPath;
use chrono::{DateTime, FixedOffset};
use log::{debug, error, warn};
use protobuf::well_known_types::Struct;
use proxy_wasm::hostcalls;
use serde_json::Value;

pub const KUADRANT_NAMESPACE: &str = "kuadrant";
Expand Down Expand Up @@ -120,7 +119,7 @@ where
}

pub fn set_attribute(attr: &str, value: &[u8]) {
match hostcalls::set_property(PropertyPath::from(attr).tokens(), Some(value)) {
match crate::data::property::set_property(PropertyPath::from(attr), Some(value)) {
Ok(_) => (),
Err(_) => error!("set_attribute: failed to set property {attr}"),
};
Expand Down
19 changes: 18 additions & 1 deletion src/data/cel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use cel_parser::{parse, Expression as CelExpression, Member, ParseError};
use chrono::{DateTime, FixedOffset};
#[cfg(feature = "debug-host-behaviour")]
use log::debug;
use log::warn;
use log::{error, warn};
use proxy_wasm::types::{Bytes, Status};
use serde_json::Value as JsonValue;
use std::borrow::Cow;
Expand Down Expand Up @@ -235,6 +235,23 @@ impl Predicate {
}
}

pub trait PredicateVec {
fn apply(&self) -> bool;
}

impl PredicateVec for Vec<Predicate> {
fn apply(&self) -> bool {
self.is_empty()
|| self.iter().all(|predicate| match predicate.test() {
Ok(b) => b,
Err(err) => {
error!("Failed to evaluate {:?}: {}", predicate, err);
panic!("Err out of this!")
}
})
}
}

pub struct Attribute {
path: Path,
cel_type: Option<ValueType>,
Expand Down
1 change: 1 addition & 0 deletions src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pub use cel::debug_all_well_known_attributes;

pub use cel::Expression;
pub use cel::Predicate;
pub use cel::PredicateVec;

pub use property::Path as PropertyPath;
18 changes: 18 additions & 0 deletions src/data/property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ pub fn host_get_map(path: &Path) -> Result<HashMap<String, String>, String> {
}
}

#[cfg(test)]
pub fn host_set_property(path: Path, value: Option<&[u8]>) -> Result<(), Status> {
debug!("set_property: {:?}", path);
let data = value.map(|bytes| bytes.to_vec()).unwrap_or_default();
test::TEST_PROPERTY_VALUE.set(Some((path, data)));
Ok(())
}

#[cfg(not(test))]
pub fn host_get_map(path: &Path) -> Result<HashMap<String, String>, String> {
match *path.tokens() {
Expand All @@ -77,6 +85,12 @@ pub(super) fn host_get_property(path: &Path) -> Result<Option<Vec<u8>>, Status>
proxy_wasm::hostcalls::get_property(path.tokens())
}

#[cfg(not(test))]
pub(super) fn host_set_property(path: Path, value: Option<&[u8]>) -> Result<(), Status> {
debug!("set_property: {:?}", path);
proxy_wasm::hostcalls::set_property(path.tokens(), value)
}

pub(super) fn get_property(path: &Path) -> Result<Option<Vec<u8>>, Status> {
match *path.tokens() {
["source", "remote_address"] => remote_address(),
Expand All @@ -85,6 +99,10 @@ pub(super) fn get_property(path: &Path) -> Result<Option<Vec<u8>>, Status> {
}
}

pub(super) fn set_property(path: Path, value: Option<&[u8]>) -> Result<(), Status> {
host_set_property(path, value)
}

#[derive(Clone, Hash, PartialEq, Eq)]
pub struct Path {
tokens: Vec<String>,
Expand Down
7 changes: 5 additions & 2 deletions src/envoy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ pub use {
AttributeContext, AttributeContext_HttpRequest, AttributeContext_Peer,
AttributeContext_Request,
},
base::Metadata,
base::{HeaderValue, HeaderValueOption, Metadata},
external_auth::{CheckRequest, CheckResponse, CheckResponse_oneof_http_response},
http_status::StatusCode,
ratelimit::{RateLimitDescriptor, RateLimitDescriptor_Entry},
rls::{RateLimitRequest, RateLimitResponse, RateLimitResponse_Code},
};

#[cfg(test)]
pub use base::HeaderValue;
pub use {
external_auth::{DeniedHttpResponse, OkHttpResponse},
http_status::HttpStatus,
};
Loading

0 comments on commit ff60de9

Please sign in to comment.