From 38d41252e62ff24df65507749683e70910100cfe Mon Sep 17 00:00:00 2001 From: Quentin de Quelen Date: Tue, 14 Apr 2020 18:00:35 +0200 Subject: [PATCH] add authentication middleware --- Cargo.lock | 1 + meilisearch-http/Cargo.toml | 1 + meilisearch-http/src/error.rs | 5 +- meilisearch-http/src/main.rs | 21 ++-- meilisearch-http/src/routes/mod.rs | 183 ++++++++++++++--------------- 5 files changed, 105 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe2b67b7d..9b2f212af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,6 +1505,7 @@ dependencies = [ "actix-files", "actix-http", "actix-rt", + "actix-service", "actix-web", "assert-json-diff", "chrono", diff --git a/meilisearch-http/Cargo.toml b/meilisearch-http/Cargo.toml index ce0bf934b..7d3ae7695 100644 --- a/meilisearch-http/Cargo.toml +++ b/meilisearch-http/Cargo.toml @@ -46,6 +46,7 @@ actix-web = "2" actix-http = "1" actix-files = "0.2.1" actix-cors = "0.2.0" +actix-service = "1.0.5" tokio = { version = "0.2.0", features = ["macros"] } [dev-dependencies] diff --git a/meilisearch-http/src/error.rs b/meilisearch-http/src/error.rs index 6c3210e1b..469140f55 100644 --- a/meilisearch-http/src/error.rs +++ b/meilisearch-http/src/error.rs @@ -9,6 +9,7 @@ use serde_json::json; pub enum ResponseError { Internal(String), BadRequest(String), + MissingAuthorizationHeader, InvalidToken(String), NotFound(String), IndexNotFound(String), @@ -27,6 +28,7 @@ impl fmt::Display for ResponseError { match self { Self::Internal(err) => write!(f, "Internal server error: {}", err), Self::BadRequest(err) => write!(f, "Bad request: {}", err), + Self::MissingAuthorizationHeader => write!(f, "You must have an authorization token"), Self::InvalidToken(err) => write!(f, "Invalid API key: {}", err), Self::NotFound(err) => write!(f, "{} not found", err), Self::IndexNotFound(index_uid) => write!(f, "Index {} not found", index_uid), @@ -53,7 +55,8 @@ impl aweb::error::ResponseError for ResponseError { match *self { Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::BadRequest(_) => StatusCode::BAD_REQUEST, - Self::InvalidToken(_) => StatusCode::FORBIDDEN, + Self::MissingAuthorizationHeader => StatusCode::FORBIDDEN, + Self::InvalidToken(_) => StatusCode::UNAUTHORIZED, Self::NotFound(_) => StatusCode::NOT_FOUND, Self::IndexNotFound(_) => StatusCode::NOT_FOUND, Self::DocumentNotFound(_) => StatusCode::NOT_FOUND, diff --git a/meilisearch-http/src/main.rs b/meilisearch-http/src/main.rs index 0e43e15f1..6ea356b63 100644 --- a/meilisearch-http/src/main.rs +++ b/meilisearch-http/src/main.rs @@ -1,7 +1,7 @@ use std::{env, thread}; use actix_cors::Cors; -use actix_web::{web, App, HttpServer, middleware}; +use actix_web::{middleware, web, App, HttpServer}; use log::info; use main_error::MainError; use meilisearch_http::data::Data; @@ -54,15 +54,21 @@ async fn main() -> Result<(), MainError> { App::new() .wrap( Cors::new() - .send_wildcard() - .allowed_header("x-meili-api-key") - .finish() + .send_wildcard() + .allowed_header("x-meili-api-key") + .finish(), ) .wrap(middleware::Logger::default()) .wrap(middleware::Compress::default()) .app_data(web::Data::new(data.clone())) + .wrap(routes::Authentication::Public) .service(routes::load_html) .service(routes::load_css) + .service(routes::search::search_with_url_query) + .service(routes::search::search_multi_index) + .service(routes::document::get_document) + .service(routes::document::get_all_documents) + .wrap(routes::Authentication::Private) .service(routes::index::list_indexes) .service(routes::index::get_index) .service(routes::index::create_index) @@ -70,11 +76,7 @@ async fn main() -> Result<(), MainError> { .service(routes::index::delete_index) .service(routes::index::get_update_status) .service(routes::index::get_all_updates_status) - .service(routes::search::search_with_url_query) - .service(routes::search::search_multi_index) - .service(routes::document::get_document) .service(routes::document::delete_document) - .service(routes::document::get_all_documents) .service(routes::document::add_documents) .service(routes::document::update_documents) .service(routes::document::delete_documents) @@ -102,7 +104,6 @@ async fn main() -> Result<(), MainError> { .service(routes::synonym::get) .service(routes::synonym::update) .service(routes::synonym::delete) - .service(routes::key::list) .service(routes::stats::index_stats) .service(routes::stats::get_stats) .service(routes::stats::get_version) @@ -110,6 +111,8 @@ async fn main() -> Result<(), MainError> { .service(routes::stats::get_sys_info_pretty) .service(routes::health::get_health) .service(routes::health::change_healthyness) + .wrap(routes::Authentication::Admin) + .service(routes::key::list) }) .bind(opt.http_addr)? .run() diff --git a/meilisearch-http/src/routes/mod.rs b/meilisearch-http/src/routes/mod.rs index 66cabd52d..5d92e185f 100644 --- a/meilisearch-http/src/routes/mod.rs +++ b/meilisearch-http/src/routes/mod.rs @@ -1,8 +1,17 @@ +use std::cell::RefCell; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll}; + +use actix_service::{Service, Transform}; +use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error}; use actix_web::{get, HttpResponse}; +use futures::future::{err, ok, Future, Ready}; use log::error; use meilisearch_core::ProcessedUpdateResult; use serde::{Deserialize, Serialize}; +use crate::error::ResponseError; use crate::Data; pub mod document; @@ -79,109 +88,91 @@ pub fn index_update_callback(index_uid: &str, data: &Data, status: ProcessedUpda } } -// pub fn load_routes(app: &mut tide::Server) { -// app.at("/").get(|_| async { -// tide::Response::new(200) -// .body_string() -// .set_mime(mime::TEXT_HTML_UTF_8) -// }); -// app.at("/bulma.min.css").get(|_| async { -// tide::Response::new(200) -// .body_string(include_str!("../../public/bulma.min.css").to_string()) -// .set_mime(mime::TEXT_CSS_UTF_8) -// }); +#[derive(Clone)] +pub enum Authentication { + Public, + Private, + Admin, +} -// app.at("/indexes") -// .get(|ctx| into_response(index::list_indexes(ctx))) -// .post(|ctx| into_response(index::create_index(ctx))); +impl Transform for Authentication +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = LoggingMiddleware; + type Future = Ready>; -// app.at("/indexes/search") -// .post(|ctx| into_response(search::search_multi_index(ctx))); + fn new_transform(&self, service: S) -> Self::Future { + ok(LoggingMiddleware { + acl: (*self).clone(), + service: Rc::new(RefCell::new(service)), + }) + } +} -// app.at("/indexes/:index") -// .get(|ctx| into_response(index::get_index(ctx))) -// .put(|ctx| into_response(index::update_index(ctx))) -// .delete(|ctx| into_response(index::delete_index(ctx))); +pub struct LoggingMiddleware { + acl: Authentication, + service: Rc>, +} -// app.at("/indexes/:index/search") -// .get(|ctx| into_response(search::search_with_url_query(ctx))); +impl Service for LoggingMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = Pin>>>; -// app.at("/indexes/:index/updates") -// .get(|ctx| into_response(index::get_all_updates_status(ctx))); + fn poll_ready(&mut self, cx: &mut Context) -> Poll> { + self.service.poll_ready(cx) + } -// app.at("/indexes/:index/updates/:update_id") -// .get(|ctx| into_response(index::get_update_status(ctx))); + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let mut svc = self.service.clone(); + let data = req.app_data::().unwrap(); -// app.at("/indexes/:index/documents") -// .get(|ctx| into_response(document::get_all_documents(ctx))) -// .post(|ctx| into_response(document::add_or_replace_multiple_documents(ctx))) -// .put(|ctx| into_response(document::add_or_update_multiple_documents(ctx))) -// .delete(|ctx| into_response(document::clear_all_documents(ctx))); + if data.api_keys.master.is_none() { + return Box::pin(svc.call(req)); + } -// app.at("/indexes/:index/documents/:document_id") -// .get(|ctx| into_response(document::get_document(ctx))) -// .delete(|ctx| into_response(document::delete_document(ctx))); + let auth_header = match req.headers().get("X-Meili-API-Key") { + Some(auth) => match auth.to_str() { + Ok(auth) => auth, + Err(_) => return Box::pin(err(ResponseError::MissingAuthorizationHeader.into())), + }, + None => { + return Box::pin(err(ResponseError::MissingAuthorizationHeader.into())); + } + }; -// app.at("/indexes/:index/documents/delete-batch") -// .post(|ctx| into_response(document::delete_multiple_documents(ctx))); + let authenticated = match self.acl { + Authentication::Admin => data.api_keys.master.as_deref() == Some(auth_header), + Authentication::Private => { + data.api_keys.master.as_deref() == Some(auth_header) + || data.api_keys.private.as_deref() == Some(auth_header) + } + Authentication::Public => { + data.api_keys.master.as_deref() == Some(auth_header) + || data.api_keys.private.as_deref() == Some(auth_header) + || data.api_keys.public.as_deref() == Some(auth_header) + } + }; -// app.at("/indexes/:index/settings") -// .get(|ctx| into_response(setting::get_all(ctx))) -// .post(|ctx| into_response(setting::update_all(ctx))) -// .delete(|ctx| into_response(setting::delete_all(ctx))); - -// app.at("/indexes/:index/settings/ranking-rules") -// .get(|ctx| into_response(setting::get_rules(ctx))) -// .post(|ctx| into_response(setting::update_rules(ctx))) -// .delete(|ctx| into_response(setting::delete_rules(ctx))); - -// app.at("/indexes/:index/settings/distinct-attribute") -// .get(|ctx| into_response(setting::get_distinct(ctx))) -// .post(|ctx| into_response(setting::update_distinct(ctx))) -// .delete(|ctx| into_response(setting::delete_distinct(ctx))); - -// app.at("/indexes/:index/settings/searchable-attributes") -// .get(|ctx| into_response(setting::get_searchable(ctx))) -// .post(|ctx| into_response(setting::update_searchable(ctx))) -// .delete(|ctx| into_response(setting::delete_searchable(ctx))); - -// app.at("/indexes/:index/settings/displayed-attributes") -// .get(|ctx| into_response(setting::displayed(ctx))) -// .post(|ctx| into_response(setting::update_displayed(ctx))) -// .delete(|ctx| into_response(setting::delete_displayed(ctx))); - -// app.at("/indexes/:index/settings/accept-new-fields") -// .get(|ctx| into_response(setting::get_accept_new_fields(ctx))) -// .post(|ctx| into_response(setting::update_accept_new_fields(ctx))); - -// app.at("/indexes/:index/settings/synonyms") -// .get(|ctx| into_response(synonym::get(ctx))) -// .post(|ctx| into_response(synonym::update(ctx))) -// .delete(|ctx| into_response(synonym::delete(ctx))); - -// app.at("/indexes/:index/settings/stop-words") -// .get(|ctx| into_response(stop_words::get(ctx))) -// .post(|ctx| into_response(stop_words::update(ctx))) -// .delete(|ctx| into_response(stop_words::delete(ctx))); - -// app.at("/indexes/:index/stats") -// .get(|ctx| into_response(stats::index_stats(ctx))); - -// app.at("/keys").get(|ctx| into_response(key::list(ctx))); - -// app.at("/health") -// .get(|ctx| into_response(health::get_health(ctx))) -// .put(|ctx| into_response(health::change_healthyness(ctx))); - -// app.at("/stats") -// .get(|ctx| into_response(stats::get_stats(ctx))); - -// app.at("/version") -// .get(|ctx| into_response(stats::get_version(ctx))); - -// app.at("/sys-info") -// .get(|ctx| into_response(stats::get_sys_info(ctx))); - -// app.at("/sys-info/pretty") -// .get(|ctx| into_response(stats::get_sys_info_pretty(ctx))); -// } + if authenticated { + Box::pin(svc.call(req)) + } else { + Box::pin(err( + ResponseError::InvalidToken(auth_header.to_string()).into() + )) + } + } +}