#![allow(clippy::complexity)]
use super::*;
use crate::dua::extractor::actix::DUAs;
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{Error, HttpResponse};
use futures::future::{ok, Either, Ready};
use rayon::prelude::*;
use reqwest::StatusCode;
use std::task::{Context, Poll};
#[derive(Clone)]
pub struct DUAEnforcer {
validation_level: u8,
}
impl DUAEnforcer {
pub fn new(level: u8) -> Self {
Self {
validation_level: level,
}
}
pub fn set_validation(&mut self, level: u8) {
self.validation_level = level;
}
}
impl Default for DUAEnforcer {
fn default() -> DUAEnforcer {
DUAEnforcer {
validation_level: 1,
}
}
}
impl<S, B> Transform<S> for DUAEnforcer
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = DUAEnforcerMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(DUAEnforcerMiddleware {
service,
validation_level: self.validation_level,
})
}
}
pub struct DUAEnforcerMiddleware<S> {
service: S,
validation_level: u8,
}
impl<S, B> Service for DUAEnforcerMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, Ready<Result<ServiceResponse<B>, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
debug!("VALIDATION LEVEL: {}", self.validation_level);
if self.validation_level == VALIDATION_NONE {
return Either::Left(self.service.call(req));
}
match req.headers().get(DUA_HEADER) {
Some(list) => {
let duas = DUAs::duas_from_header_value(list);
let mut valid_ind: bool = false;
if self.validation_level >= VALIDATION_LOW && !duas.vec().is_empty() {
valid_ind = true;
}
if valid_ind && self.validation_level >= VALIDATION_HIGH {
let checks: usize = duas
.vec()
.par_iter()
.map(|d| match reqwest::blocking::get(&d.location.clone()) {
Ok(rsp) => {
if rsp.status() == StatusCode::OK {
1
} else {
info!("{}", format!("Invalid DUA: {}", d.location.clone()));
0
}
}
Err(_err) => {
info!("{}", format!("Invalid DUA: {}", d.location.clone()));
0
}
})
.sum();
if duas.vec().len() == checks {
valid_ind = true;
} else {
valid_ind = false;
}
}
if valid_ind {
Either::Left(self.service.call(req))
} else {
Either::Right(ok(
req.into_response(HttpResponse::BadRequest().finish().into_body())
))
}
}
None => Either::Right(ok(
req.into_response(HttpResponse::BadRequest().finish().into_body())
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::StatusCode;
use actix_web::{http, test, web, App, HttpRequest, HttpResponse};
fn index_middleware_dua(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.header(http::header::CONTENT_TYPE, "application/json")
.body(r#"{"status":"Ok"}"#)
}
#[test]
fn test_add_middleware() {
let _app = App::new()
.wrap(DUAEnforcer::default())
.service(web::resource("/").route(web::get().to(index_middleware_dua)));
assert!(true);
}
#[actix_rt::test]
async fn test_dua_none_missing() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_NONE))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_default_ok() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::default())
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_default_empty() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::default())
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_dua_default_invalid() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::default())
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://example.com/invalid.pdf","agreed_dtm": 1553988607},{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_default_missing() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::default())
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_dua_default_validation_level() {
let dflt = DUAEnforcer::default();
assert_eq!(dflt.validation_level, 1);
}
#[actix_rt::test]
async fn test_dua_valid_high_ok() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_HIGH))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_valid_high_empty() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_HIGH))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_dua_valid_high_invalid() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_HIGH))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://example.com/invalid.pdf","agreed_dtm": 1553988607},{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_dua_high_missing() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_HIGH))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_dua_low_ok() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_LOW))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_low_empty() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_LOW))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_dua_low_invalid() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_LOW))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post().uri("/")
.header("content-type", "application/json")
.header(DUA_HEADER, r#"[{"agreement_name":"patient data use","location":"https://example.com/invalid.pdf","agreed_dtm": 1553988607},{"agreement_name":"patient data use","location":"https://github.com/dsietz/pbd/blob/master/tests/duas/Patient%20Data%20Use%20Agreement.pdf","agreed_dtm": 1553988607}]"#)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_dua_low_missing() {
let mut app = test::init_service(
App::new()
.wrap(DUAEnforcer::new(VALIDATION_LOW))
.route("/", web::post().to(index_middleware_dua)),
)
.await;
let req = test::TestRequest::post()
.uri("/")
.header("content-type", "application/json")
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}