use std::sync::Arc;
use actix_web::{
error::ErrorBadRequest,
http::{header, Method as ActixMethod},
web, Error as ActixError, HttpRequest, HttpResponse,
};
use futures::{future::IntoFuture, stream::Stream, Future};
use protobuf::{self, Message};
use std::cmp::PartialEq;
#[cfg(feature = "authorization")]
use crate::rest_api::auth::authorization::{Permission, PermissionMap};
use super::{Continuation, RequestGuard};
#[derive(Clone, PartialEq)]
pub enum Method {
Get,
Post,
Put,
Patch,
Delete,
Head,
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Method::Get => f.write_str("GET"),
Method::Post => f.write_str("POST"),
Method::Put => f.write_str("PUT"),
Method::Patch => f.write_str("PATCH"),
Method::Delete => f.write_str("DELETE"),
Method::Head => f.write_str("HEAD"),
}
}
}
impl PartialEq<ActixMethod> for Method {
fn eq(&self, other: &ActixMethod) -> bool {
match self {
Method::Get => other == ActixMethod::GET,
Method::Post => other == ActixMethod::POST,
Method::Put => other == ActixMethod::PUT,
Method::Patch => other == ActixMethod::PATCH,
Method::Delete => other == ActixMethod::DELETE,
Method::Head => other == ActixMethod::HEAD,
}
}
}
pub fn into_bytes(payload: web::Payload) -> impl Future<Item = Vec<u8>, Error = ActixError> {
payload
.from_err::<ActixError>()
.fold(web::BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, ActixError>(body)
})
.and_then(|body| Ok(body.to_vec()))
.into_future()
}
pub fn into_protobuf<M: Message>(
payload: web::Payload,
) -> impl Future<Item = M, Error = ActixError> {
payload
.from_err::<ActixError>()
.fold(web::BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, ActixError>(body)
})
.and_then(|body| match Message::parse_from_bytes(&body) {
Ok(proto) => Ok(proto),
Err(err) => Err(ErrorBadRequest(json!({ "message": format!("{}", err) }))),
})
.into_future()
}
pub type HandlerFunction = Box<
dyn Fn(HttpRequest, web::Payload) -> Box<dyn Future<Item = HttpResponse, Error = ActixError>>
+ Send
+ Sync
+ 'static,
>;
#[cfg(feature = "authorization")]
#[derive(Clone)]
struct ResourceMethod {
method: Method,
permission: Permission,
handler: Arc<HandlerFunction>,
}
#[derive(Clone)]
pub struct Resource {
route: String,
request_guards: Vec<Arc<dyn RequestGuard>>,
#[cfg(feature = "authorization")]
methods: Vec<ResourceMethod>,
#[cfg(not(feature = "authorization"))]
methods: Vec<(Method, Arc<HandlerFunction>)>,
}
impl Resource {
#[cfg(not(feature = "authorization"))]
#[deprecated(note = "Please use the `build` and `add_method` methods instead")]
pub fn new<F>(method: Method, route: &str, handle: F) -> Self
where
F: Fn(
HttpRequest,
web::Payload,
) -> Box<dyn Future<Item = HttpResponse, Error = ActixError>>
+ Send
+ Sync
+ 'static,
{
Self::build(route).add_method(method, handle)
}
pub fn build(route: &str) -> Self {
Self {
route: route.to_string(),
methods: vec![],
request_guards: vec![],
}
}
#[cfg(feature = "authorization")]
pub fn add_method<F>(mut self, method: Method, permission: Permission, handle: F) -> Self
where
F: Fn(
HttpRequest,
web::Payload,
) -> Box<dyn Future<Item = HttpResponse, Error = ActixError>>
+ Send
+ Sync
+ 'static,
{
self.methods.push(ResourceMethod {
method,
permission,
handler: Arc::new(Box::new(handle)),
});
self
}
#[cfg(not(feature = "authorization"))]
pub fn add_method<F>(mut self, method: Method, handle: F) -> Self
where
F: Fn(
HttpRequest,
web::Payload,
) -> Box<dyn Future<Item = HttpResponse, Error = ActixError>>
+ Send
+ Sync
+ 'static,
{
self.methods.push((method, Arc::new(Box::new(handle))));
self
}
pub fn add_request_guard<RG>(mut self, guard: RG) -> Self
where
RG: RequestGuard + Clone + 'static,
{
self.request_guards.push(Arc::new(guard));
self
}
#[cfg(feature = "authorization")]
pub(super) fn into_route(self) -> (actix_web::Resource, PermissionMap) {
let mut resource = web::resource(&self.route);
let mut allowed_methods = self
.methods
.iter()
.map(|resource_method| resource_method.method.to_string())
.collect::<Vec<_>>()
.join(", ");
allowed_methods += ", OPTIONS";
resource = resource.route(web::route().guard(actix_web::guard::Options()).to(
move |_: HttpRequest| {
HttpResponse::Ok()
.header(header::ALLOW, allowed_methods.clone())
.finish()
},
));
let request_guards = self.request_guards;
let mut permission_map = PermissionMap::new();
let route = self.route.clone();
let resource = self.methods.into_iter().fold(
resource,
|resource,
ResourceMethod {
method,
permission,
handler,
}| {
let guards = request_guards.clone();
let func = move |r: HttpRequest, p: web::Payload| {
if !guards.is_empty() {
for guard in guards.clone() {
match guard.evaluate(&r) {
Continuation::Terminate(result) => return result,
Continuation::Continue => (),
}
}
}
(handler)(r, p)
};
permission_map.add_permission(method.clone(), &route, permission);
resource.route(match method {
Method::Get => web::get().to_async(func),
Method::Post => web::post().to_async(func),
Method::Put => web::put().to_async(func),
Method::Patch => web::patch().to_async(func),
Method::Delete => web::delete().to_async(func),
Method::Head => web::head().to_async(func),
})
},
);
(resource, permission_map)
}
#[cfg(not(feature = "authorization"))]
pub(super) fn into_route(self) -> actix_web::Resource {
let mut resource = web::resource(&self.route);
let mut allowed_methods = self
.methods
.iter()
.map(|(method, _)| method.to_string())
.collect::<Vec<_>>()
.join(", ");
allowed_methods += ", OPTIONS";
resource = resource.route(web::route().guard(actix_web::guard::Options()).to(
move |_: HttpRequest| {
HttpResponse::Ok()
.header(header::ALLOW, allowed_methods.clone())
.finish()
},
));
let request_guards = self.request_guards;
self.methods
.into_iter()
.fold(resource, |resource, (method, handler)| {
let guards = request_guards.clone();
let func = move |r: HttpRequest, p: web::Payload| {
if !guards.is_empty() {
for guard in guards.clone() {
match guard.evaluate(&r) {
Continuation::Terminate(result) => return result,
Continuation::Continue => (),
}
}
}
(handler)(r, p)
};
resource.route(match method {
Method::Get => web::get().to_async(func),
Method::Post => web::post().to_async(func),
Method::Put => web::put().to_async(func),
Method::Patch => web::patch().to_async(func),
Method::Delete => web::delete().to_async(func),
Method::Head => web::head().to_async(func),
})
})
}
}
pub trait RestResourceProvider {
fn resources(&self) -> Vec<Resource>;
}
#[cfg(test)]
mod test {
use super::*;
use actix_http::Response;
use futures::IntoFuture;
#[test]
fn test_resource() {
let mut resource = Resource::build("/test");
#[cfg(feature = "authorization")]
{
resource = resource.add_method(
Method::Get,
Permission::AllowUnauthenticated,
|_: HttpRequest, _: web::Payload| Box::new(Response::Ok().finish().into_future()),
);
}
#[cfg(not(feature = "authorization"))]
{
resource = resource.add_method(Method::Get, |_: HttpRequest, _: web::Payload| {
Box::new(Response::Ok().finish().into_future())
});
}
resource.into_route();
}
#[test]
fn test_resource_with_guard() {
let mut resource = Resource::build("/test-guarded").add_request_guard(|_: &HttpRequest| {
Continuation::terminate(Response::BadRequest().finish().into_future())
});
#[cfg(feature = "authorization")]
{
resource = resource.add_method(
Method::Get,
Permission::AllowUnauthenticated,
|_: HttpRequest, _: web::Payload| Box::new(Response::Ok().finish().into_future()),
);
}
#[cfg(not(feature = "authorization"))]
{
resource = resource.add_method(Method::Get, |_: HttpRequest, _: web::Payload| {
Box::new(Response::Ok().finish().into_future())
});
}
resource.into_route();
}
#[cfg(feature = "authorization")]
#[test]
fn test_resource_permission() {
let permission = Permission::Check {
permission_id: "test",
permission_display_name: "",
permission_description: "",
};
let (_, permission_map) = Resource::build("/test")
.add_method(
Method::Get,
permission,
|_: HttpRequest, _: web::Payload| Box::new(Response::Ok().finish().into_future()),
)
.into_route();
assert_eq!(
permission_map
.get_permission(&Method::Get, "/test")
.expect("Missing permission"),
&permission,
);
}
}