use actix_web::dev::*;
use actix_web::{
http::{
header::{self, HeaderValue},
Method as ActixMethod,
},
Error as ActixError, HttpMessage, HttpResponse,
};
use futures::{
future::{ok, FutureResult},
Future, IntoFuture, Poll,
};
use crate::rest_api::ErrorResponse;
#[cfg(feature = "authorization")]
use crate::rest_api::Method;
#[cfg(feature = "authorization")]
use super::authorization::{AuthorizationHandler, PermissionMap};
use super::{authorize, identity::IdentityProvider, AuthorizationResult};
#[derive(Clone)]
pub struct Authorization {
identity_providers: Vec<Box<dyn IdentityProvider>>,
#[cfg(feature = "authorization")]
authorization_handlers: Vec<Box<dyn AuthorizationHandler>>,
}
impl Authorization {
pub fn new(
identity_providers: Vec<Box<dyn IdentityProvider>>,
#[cfg(feature = "authorization")] authorization_handlers: Vec<
Box<dyn AuthorizationHandler>,
>,
) -> Self {
Self {
identity_providers,
#[cfg(feature = "authorization")]
authorization_handlers,
}
}
}
impl<S, B> Transform<S> for Authorization
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = AuthorizationMiddleware<S>;
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthorizationMiddleware {
identity_providers: self.identity_providers.clone(),
#[cfg(feature = "authorization")]
authorization_handlers: self.authorization_handlers.clone(),
service,
})
}
}
pub struct AuthorizationMiddleware<S> {
identity_providers: Vec<Box<dyn IdentityProvider>>,
#[cfg(feature = "authorization")]
authorization_handlers: Vec<Box<dyn AuthorizationHandler>>,
service: S,
}
impl<S, B> Service for AuthorizationMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
self.service.poll_ready()
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if req.method() == ActixMethod::OPTIONS {
return Box::new(self.service.call(req).and_then(|mut res| {
res.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
HeaderValue::from_static("true"),
);
res
}));
}
#[cfg(feature = "authorization")]
let method = match *req.method() {
ActixMethod::GET => Method::Get,
ActixMethod::POST => Method::Post,
ActixMethod::PUT => Method::Put,
ActixMethod::PATCH => Method::Patch,
ActixMethod::DELETE => Method::Delete,
ActixMethod::HEAD => Method::Head,
_ => {
return Box::new(
req.into_response(
HttpResponse::BadRequest()
.json(ErrorResponse::bad_request(
"HTTP method not supported by Splinter REST API",
))
.into_body(),
)
.into_future(),
)
}
};
let auth_header =
match req
.headers()
.get("Authorization")
.map(|auth| auth.to_str())
.transpose()
{
Ok(opt) => opt,
Err(_) => return Box::new(
req.into_response(
HttpResponse::BadRequest()
.json(ErrorResponse::bad_request(
"Authorization header must contain only visible ASCII characters",
))
.into_body(),
)
.into_future(),
),
};
#[cfg(feature = "authorization")]
let permission_map = match req.app_data::<PermissionMap>() {
Some(map) => map,
None => {
error!("Missing REST API permission map");
return Box::new(
req.into_response(
HttpResponse::InternalServerError()
.json(ErrorResponse::internal_error())
.into_body(),
)
.into_future(),
);
}
};
match authorize(
#[cfg(feature = "authorization")]
&method,
req.path(),
auth_header,
#[cfg(feature = "authorization")]
permission_map.get_ref(),
&self.identity_providers,
#[cfg(feature = "authorization")]
&self.authorization_handlers,
) {
AuthorizationResult::Authorized(identity) => {
debug!("Authenticated user {:?}", identity);
req.extensions_mut().insert(identity);
}
#[cfg(any(
feature = "authorization",
feature = "biome-credentials",
feature = "oauth"
))]
AuthorizationResult::NoAuthorizationNecessary => {}
AuthorizationResult::Unauthorized => {
return Box::new(
req.into_response(
HttpResponse::Unauthorized()
.json(ErrorResponse::unauthorized())
.into_body(),
)
.into_future(),
)
}
#[cfg(feature = "authorization")]
AuthorizationResult::UnknownEndpoint => {
return Box::new(
req.into_response(
HttpResponse::NotFound()
.json(ErrorResponse::not_found("endpoint not found"))
.into_body(),
)
.into_future(),
)
}
}
Box::new(self.service.call(req).and_then(|mut res| {
res.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
HeaderValue::from_static("true"),
);
res
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{http::StatusCode, test, web, App, HttpRequest};
use crate::error::InternalError;
#[cfg(feature = "authorization")]
use crate::rest_api::auth::authorization::Permission;
use crate::rest_api::auth::{identity::Identity, AuthorizationHeader};
#[test]
fn auth_middleware_options_request_header() {
let mut app = test::init_service(
App::new()
.wrap(Authorization::new(
vec![],
#[cfg(feature = "authorization")]
vec![],
))
.route(
"/",
web::route()
.method(ActixMethod::OPTIONS)
.to(|| HttpResponse::Ok()),
),
);
let req = test::TestRequest::with_uri("/")
.method(ActixMethod::OPTIONS)
.to_request();
let resp = test::block_on(app.call(req)).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("Access-Control-Allow-Credentials"),
Some(&HeaderValue::from_static("true"))
);
}
#[test]
fn auth_middleware_unauthorized() {
let app = App::new()
.wrap(Authorization::new(
vec![],
#[cfg(feature = "authorization")]
vec![],
))
.route("/", web::get().to(|| HttpResponse::Ok()));
#[cfg(feature = "authorization")]
let app = {
let mut permission_map = PermissionMap::default();
permission_map.add_permission(Method::Get, "/", Permission::AllowAuthenticated);
app.data(permission_map)
};
let mut service = test::init_service(app);
let req = test::TestRequest::with_uri("/").to_request();
let resp = test::block_on(service.call(req)).unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn auth_middleware_not_found() {
let app = App::new()
.wrap(Authorization::new(
vec![Box::new(AlwaysAcceptIdentityProvider)],
#[cfg(feature = "authorization")]
vec![],
))
.route("/", web::get().to(|| HttpResponse::Ok()));
#[cfg(feature = "authorization")]
let app = {
let mut permission_map = PermissionMap::default();
permission_map.add_permission(Method::Get, "/", Permission::AllowAuthenticated);
app.data(permission_map)
};
let mut service = test::init_service(app);
let req = test::TestRequest::with_uri("/test")
.header("Authorization", "test")
.to_request();
let resp = test::block_on(service.call(req)).unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[test]
fn auth_middleware_authorized() {
let auth_middleware = Authorization::new(
vec![Box::new(AlwaysAcceptIdentityProvider)],
#[cfg(feature = "authorization")]
vec![],
);
let app = App::new().wrap(auth_middleware).route(
"/",
web::get().to(|req: HttpRequest| {
if req.extensions().get() == Some(&Identity::Custom("identity".into())) {
HttpResponse::Ok()
} else {
HttpResponse::InternalServerError()
}
}),
);
#[cfg(feature = "authorization")]
let app = {
let mut permission_map = PermissionMap::default();
permission_map.add_permission(Method::Get, "/", Permission::AllowAuthenticated);
app.data(permission_map)
};
let mut service = test::init_service(app);
let req = test::TestRequest::with_uri("/")
.header("Authorization", "test")
.to_request();
let resp = test::block_on(service.call(req)).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[derive(Clone)]
struct AlwaysAcceptIdentityProvider;
impl IdentityProvider for AlwaysAcceptIdentityProvider {
fn get_identity(
&self,
_authorization: &AuthorizationHeader,
) -> Result<Option<Identity>, InternalError> {
Ok(Some(Identity::Custom("identity".into())))
}
fn clone_box(&self) -> Box<dyn IdentityProvider> {
Box::new(self.clone())
}
}
}