use std::sync::Arc;
pub const CUSTOM_REQUEST_CHECK_NAME: &str = "custom_request";
pub const CUSTOM_VALIDATORS_CHECK_NAME: &str = "custom_validators";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CustomRequestContext<'a> {
pub method: &'a str,
pub path: &'a str,
pub client_ip: Option<&'a str>,
pub body: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomResponse {
pub status: Option<u16>,
pub body: Option<String>,
}
pub type CustomRequestFn =
Arc<dyn Fn(&CustomRequestContext<'_>) -> Option<CustomResponse> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidatorAnswer {
TruthyNonResponse,
Response(CustomResponse),
}
pub type CustomValidatorFn =
Arc<dyn Fn(&CustomRequestContext<'_>) -> Option<ValidatorAnswer> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CustomRequestVerdict {
Allowed,
Blocked {
status: Option<u16>,
function: String,
},
}
#[must_use]
pub fn decide_custom_request(
check: &CustomRequestFn,
function_name: &str,
ctx: &CustomRequestContext<'_>,
) -> CustomRequestVerdict {
if let Some(response) = check(ctx) {
return CustomRequestVerdict::Blocked {
status: response.status,
function: function_name.to_owned(),
};
}
CustomRequestVerdict::Allowed
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CustomValidatorsVerdict {
Allowed,
Failed {
block: Option<CustomResponse>,
validator: String,
},
}
#[must_use]
pub fn decide_custom_validators(
validators: &[(String, CustomValidatorFn)],
ctx: &CustomRequestContext<'_>,
) -> CustomValidatorsVerdict {
for (name, validator) in validators {
let Some(answer) = validator(ctx) else {
continue;
};
return CustomValidatorsVerdict::Failed {
block: match answer {
ValidatorAnswer::Response(response) => Some(response),
ValidatorAnswer::TruthyNonResponse => None,
},
validator: name.clone(),
};
}
CustomValidatorsVerdict::Allowed
}
#[must_use]
pub fn anonymous_name() -> String {
String::from("anonymous")
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx<'a>() -> CustomRequestContext<'a> {
CustomRequestContext {
method: "GET",
path: "/private",
client_ip: Some("192.0.2.9"),
body: None,
}
}
#[test]
fn custom_request_passes_and_blocks() {
let ctx = ctx();
let pass: CustomRequestFn = Arc::new(|_ctx| None);
assert_eq!(
decide_custom_request(&pass, "my_check", &ctx),
CustomRequestVerdict::Allowed
);
let block: CustomRequestFn = Arc::new(|_ctx| {
Some(CustomResponse {
status: Some(403),
body: None,
})
});
assert_eq!(
decide_custom_request(&block, "my_check", &ctx),
CustomRequestVerdict::Blocked {
status: Some(403),
function: "my_check".to_owned()
}
);
let unknown: CustomRequestFn = Arc::new(|_ctx| {
Some(CustomResponse {
status: None,
body: None,
})
});
assert_eq!(
decide_custom_request(&unknown, "my_check", &ctx),
CustomRequestVerdict::Blocked {
status: None,
function: "my_check".to_owned()
}
);
}
#[test]
fn validators_run_in_order_and_first_truthy_wins() {
let ctx = ctx();
let never: CustomValidatorFn = Arc::new(|_ctx| None);
let blocking: CustomValidatorFn = Arc::new(|_ctx| {
Some(ValidatorAnswer::Response(CustomResponse {
status: Some(403),
body: None,
}))
});
let flagging: CustomValidatorFn = Arc::new(|_ctx| Some(ValidatorAnswer::TruthyNonResponse));
let all_pass = vec![
(String::from("first"), never.clone()),
(String::from("second"), never.clone()),
];
assert_eq!(
decide_custom_validators(&all_pass, &ctx),
CustomValidatorsVerdict::Allowed
);
let first_blocks = vec![
(String::from("first"), never.clone()),
(String::from("second"), blocking.clone()),
(String::from("third"), flagging.clone()),
];
assert_eq!(
decide_custom_validators(&first_blocks, &ctx),
CustomValidatorsVerdict::Failed {
block: Some(CustomResponse {
status: Some(403),
body: None
}),
validator: "second".to_owned()
}
);
let first_flags = vec![
(String::from("first"), flagging.clone()),
(String::from("second"), blocking.clone()),
];
assert_eq!(
decide_custom_validators(&first_flags, &ctx),
CustomValidatorsVerdict::Failed {
block: None,
validator: "first".to_owned()
}
);
}
#[test]
fn a_truthy_non_response_logs_but_never_blocks() {
let ctx = ctx();
let flagging: CustomValidatorFn = Arc::new(|_ctx| Some(ValidatorAnswer::TruthyNonResponse));
let validators = vec![(String::from("flagger"), flagging)];
assert_eq!(
decide_custom_validators(&validators, &ctx),
CustomValidatorsVerdict::Failed {
block: None,
validator: "flagger".to_owned()
},
"the reference returns the validator's response only when it is a \
real GuardResponse; a truthy non-response cannot block"
);
}
#[test]
fn the_context_carries_the_request_facts() {
let seen: CustomValidatorFn = Arc::new(|ctx| {
(ctx.path == "/private" && ctx.method == "POST").then_some(ValidatorAnswer::Response(
CustomResponse {
status: Some(403),
body: None,
},
))
});
let validators = vec![(String::from("gate"), seen)];
assert_eq!(
decide_custom_validators(&validators, &ctx()),
CustomValidatorsVerdict::Allowed,
"GET /private passes the POST-only gate"
);
let post = CustomRequestContext {
method: "POST",
path: "/private",
client_ip: None,
body: None,
};
assert_eq!(
decide_custom_validators(&validators, &post),
CustomValidatorsVerdict::Failed {
block: Some(CustomResponse {
status: Some(403),
body: None
}),
validator: "gate".to_owned()
},
"a real GuardResponse both blocks and carries the response"
);
}
#[test]
fn anonymous_fallback_name() {
assert_eq!(anonymous_name(), "anonymous");
}
}