diff --git a/meilisearch-http/src/error.rs b/meilisearch-http/src/error.rs index 6124ed880..4f47abd66 100644 --- a/meilisearch-http/src/error.rs +++ b/meilisearch-http/src/error.rs @@ -10,23 +10,6 @@ use meilisearch_error::{Code, ErrorCode}; use milli::UserError; use serde::{Deserialize, Serialize}; -#[derive(Debug, thiserror::Error)] -pub enum AuthenticationError { - #[error("You must have an authorization token")] - MissingAuthorizationHeader, - #[error("Invalid API key")] - InvalidToken(String), -} - -impl ErrorCode for AuthenticationError { - fn error_code(&self) -> Code { - match self { - AuthenticationError::MissingAuthorizationHeader => Code::MissingAuthorizationHeader, - AuthenticationError::InvalidToken(_) => Code::InvalidToken, - } - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ResponseError { diff --git a/meilisearch-http/src/extractors/authentication/error.rs b/meilisearch-http/src/extractors/authentication/error.rs new file mode 100644 index 000000000..29578e373 --- /dev/null +++ b/meilisearch-http/src/extractors/authentication/error.rs @@ -0,0 +1,26 @@ +use meilisearch_error::{Code, ErrorCode}; + +#[derive(Debug, thiserror::Error)] +pub enum AuthenticationError { + #[error("You must have an authorization token")] + MissingAuthorizationHeader, + #[error("Invalid API key")] + InvalidToken(String), + // Triggered on configuration error. + #[error("Irretrievable state")] + IrretrievableState, + #[error("Unknown authentication policy")] + UnknownPolicy, +} + +impl ErrorCode for AuthenticationError { + fn error_code(&self) -> Code { + match self { + AuthenticationError::MissingAuthorizationHeader => Code::MissingAuthorizationHeader, + AuthenticationError::InvalidToken(_) => Code::InvalidToken, + AuthenticationError::IrretrievableState => Code::Internal, + AuthenticationError::UnknownPolicy => Code::Internal, + } + } +} + diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs new file mode 100644 index 000000000..6b9ac24ae --- /dev/null +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -0,0 +1,182 @@ +mod error; + +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::ops::Deref; + +use actix_web::FromRequest; +use futures::future::err; +use futures::future::{ok, Ready}; + +use crate::error::ResponseError; +use error::AuthenticationError; + +macro_rules! create_policies { + ($($name:ident), *) => { + pub mod policies { + use std::collections::HashSet; + use crate::extractors::authentication::Policy; + + $( + #[derive(Debug, Default)] + pub struct $name { + inner: HashSet> + } + + impl $name { + pub fn new() -> Self { + Self { inner: HashSet::new() } + } + + pub fn add(&mut self, token: Vec) { + &mut self.inner.insert(token); + } + } + + impl Policy for $name { + fn authenticate(&self, token: &[u8]) -> bool { + self.inner.contains(token) + } + } + )* + } + }; +} + +create_policies!(Public, Private, Admin); + +/// Instanciate a `Policies`, filled with the given policies. +macro_rules! init_policies { + ($($name:ident), *) => { + { + let mut policies = crate::extractors::authentication::Policies::new(); + $( + let policy = $name::new(); + policies.insert(policy); + )* + policies + } + }; +} + +/// Adds user to all specified policies. +macro_rules! create_users { + ($policies:ident, $($user:expr => { $($policy:ty), * }), *) => { + { + $( + $( + $policies.get_mut::<$policy>().map(|p| p.add($user.to_owned())); + )* + )* + } + }; +} + +pub struct GuardedData { + data: D, + _marker: PhantomData, +} + +impl Deref for GuardedData { + type Target = D; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +pub trait Policy { + fn authenticate(&self, token: &[u8]) -> bool; +} + +#[derive(Debug)] +pub struct Policies { + inner: HashMap>, +} + +impl Policies { + pub fn new() -> Self { + Self { + inner: HashMap::new(), + } + } + + pub fn insert(&mut self, policy: S) { + self.inner.insert(TypeId::of::(), Box::new(policy)); + } + + pub fn get(&self) -> Option<&S> { + self.inner + .get(&TypeId::of::()) + .and_then(|p| p.downcast_ref::()) + } + + pub fn get_mut(&mut self) -> Option<&mut S> { + self.inner + .get_mut(&TypeId::of::()) + .and_then(|p| p.downcast_mut::()) + } +} + +impl Default for Policies { + fn default() -> Self { + Self::new() + } +} + +pub enum AuthConfig { + NoAuth, + Auth(Policies), +} + +impl Default for AuthConfig { + fn default() -> Self { + Self::NoAuth + } +} + +impl FromRequest for GuardedData { + type Config = AuthConfig; + + type Error = ResponseError; + + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _payload: &mut actix_http::Payload, + ) -> Self::Future { + match req.app_data::() { + Some(config) => match config { + AuthConfig::NoAuth => match req.app_data::().cloned() { + Some(data) => ok(Self { + data, + _marker: PhantomData, + }), + None => err(AuthenticationError::IrretrievableState.into()), + }, + AuthConfig::Auth(policies) => match policies.get::

() { + Some(policy) => match req.headers().get("x-meili-api-key") { + Some(token) => { + if policy.authenticate(token.as_bytes()) { + match req.app_data::().cloned() { + Some(data) => ok(Self { + data, + _marker: PhantomData, + }), + None => err(AuthenticationError::IrretrievableState.into()), + } + } else { + err(AuthenticationError::InvalidToken(String::from("hello")).into()) + } + } + None => err(AuthenticationError::MissingAuthorizationHeader.into()), + }, + None => err(AuthenticationError::UnknownPolicy.into()), + }, + }, + None => err(AuthenticationError::IrretrievableState.into()), + } + } +} diff --git a/meilisearch-http/src/extractors/mod.rs b/meilisearch-http/src/extractors/mod.rs index fbb091fe2..09a56e4a0 100644 --- a/meilisearch-http/src/extractors/mod.rs +++ b/meilisearch-http/src/extractors/mod.rs @@ -1 +1,3 @@ pub mod payload; +#[macro_use] +pub mod authentication; diff --git a/meilisearch-http/src/helpers/authentication.rs b/meilisearch-http/src/helpers/authentication.rs deleted file mode 100644 index dddf57138..000000000 --- a/meilisearch-http/src/helpers/authentication.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::pin::Pin; -use std::task::{Context, Poll}; - -use actix_web::body::Body; -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::web; -use actix_web::ResponseError as _; -use futures::future::{ok, Future, Ready}; -use futures::ready; -use pin_project::pin_project; - -use crate::error::{AuthenticationError, ResponseError}; -use crate::Data; - -#[derive(Clone, Copy)] -pub enum Authentication { - Public, - Private, - Admin, -} - -impl Transform for Authentication -where - S: Service, Error = actix_web::Error>, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type InitError = (); - type Transform = LoggingMiddleware; - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(LoggingMiddleware { - acl: *self, - service, - }) - } -} - -pub struct LoggingMiddleware { - acl: Authentication, - service: S, -} - -#[allow(clippy::type_complexity)] -impl Service for LoggingMiddleware -where - S: Service, Error = actix_web::Error>, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = AuthenticationFuture; - - fn poll_ready(&self, cx: &mut Context) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let data = req.app_data::>().unwrap(); - - if data.api_keys().master.is_none() { - return AuthenticationFuture::Authenticated(self.service.call(req)); - } - - let auth_header = match req.headers().get("X-Meili-API-Key") { - Some(auth) => match auth.to_str() { - Ok(auth) => auth, - Err(_) => return AuthenticationFuture::NoHeader(Some(req)), - }, - None => return AuthenticationFuture::NoHeader(Some(req)), - }; - - 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) - } - }; - - if authenticated { - AuthenticationFuture::Authenticated(self.service.call(req)) - } else { - AuthenticationFuture::Refused(Some(req)) - } - } -} - -#[pin_project(project = AuthProj)] -pub enum AuthenticationFuture -where - S: Service, -{ - Authenticated(#[pin] S::Future), - NoHeader(Option), - Refused(Option), -} - -impl Future for AuthenticationFuture -where - S: Service, Error = actix_web::Error>, -{ - type Output = Result, actix_web::Error>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - match this { - AuthProj::Authenticated(fut) => match ready!(fut.poll(cx)) { - Ok(resp) => Poll::Ready(Ok(resp)), - Err(e) => Poll::Ready(Err(e)), - }, - AuthProj::NoHeader(req) => { - match req.take() { - Some(req) => { - let response = - ResponseError::from(AuthenticationError::MissingAuthorizationHeader); - let response = response.error_response(); - let response = req.into_response(response); - Poll::Ready(Ok(response)) - } - // https://doc.rust-lang.org/nightly/std/future/trait.Future.html#panics - None => unreachable!("poll called again on ready future"), - } - } - AuthProj::Refused(req) => { - match req.take() { - Some(req) => { - let bad_token = req - .headers() - .get("X-Meili-API-Key") - .map(|h| h.to_str().map(String::from).unwrap_or_default()) - .unwrap_or_default(); - let response = - ResponseError::from(AuthenticationError::InvalidToken(bad_token)); - let response = response.error_response(); - let response = req.into_response(response); - Poll::Ready(Ok(response)) - } - // https://doc.rust-lang.org/nightly/std/future/trait.Future.html#panics - None => unreachable!("poll called again on ready future"), - } - } - } - } -} diff --git a/meilisearch-http/src/helpers/mod.rs b/meilisearch-http/src/helpers/mod.rs index a5cddf29c..c664f15aa 100644 --- a/meilisearch-http/src/helpers/mod.rs +++ b/meilisearch-http/src/helpers/mod.rs @@ -1,6 +1,4 @@ -pub mod authentication; pub mod compression; mod env; -pub use authentication::Authentication; pub use env::EnvSizer; diff --git a/meilisearch-http/src/lib.rs b/meilisearch-http/src/lib.rs index 46fea718c..0eb61f84c 100644 --- a/meilisearch-http/src/lib.rs +++ b/meilisearch-http/src/lib.rs @@ -1,6 +1,7 @@ pub mod data; #[macro_use] pub mod error; +#[macro_use] pub mod extractors; pub mod helpers; mod index; @@ -11,17 +12,21 @@ pub mod routes; #[cfg(all(not(debug_assertions), feature = "analytics"))] pub mod analytics; +use crate::extractors::authentication::AuthConfig; + pub use self::data::Data; pub use option::Opt; use actix_web::web; +use extractors::authentication::policies::*; use extractors::payload::PayloadConfig; pub fn configure_data(config: &mut web::ServiceConfig, data: Data) { let http_payload_size_limit = data.http_payload_size_limit(); config - .data(data) + .data(data.clone()) + .app_data(data) .app_data( web::JsonConfig::default() .limit(http_payload_size_limit) @@ -35,10 +40,30 @@ pub fn configure_data(config: &mut web::ServiceConfig, data: Data) { ); } +pub fn configure_auth(config: &mut web::ServiceConfig, data: &Data) { + let keys = data.api_keys(); + let auth_config = if let Some(ref master_key) = keys.master { + let private_key = keys.private.as_ref().unwrap(); + let public_key = keys.public.as_ref().unwrap(); + let mut policies = init_policies!(Public, Private, Admin); + create_users!( + policies, + master_key.as_bytes() => { Admin, Private, Public }, + private_key.as_bytes() => { Private, Public }, + public_key.as_bytes() => { Public } + ); + AuthConfig::Auth(policies) + } else { + AuthConfig::NoAuth + }; + + config.app_data(auth_config); +} + #[cfg(feature = "mini-dashboard")] pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) { - use actix_web_static_files::Resource; use actix_web::HttpResponse; + use actix_web_static_files::Resource; mod generated { include!(concat!(env!("OUT_DIR"), "/generated.rs")); @@ -46,30 +71,32 @@ pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) { if enable_frontend { let generated = generated::generate(); - let mut scope = web::scope("/"); - // Generate routes for mini-dashboard assets - for (path, resource) in generated.into_iter() { - let Resource {mime_type, data, ..} = resource; - // Redirect index.html to / - if path == "index.html" { - config.service(web::resource("/").route(web::get().to(move || { - HttpResponse::Ok().content_type(mime_type).body(data) - }))); - } else { - scope = scope.service(web::resource(path).route(web::get().to(move || { - HttpResponse::Ok().content_type(mime_type).body(data) - }))); - } + let mut scope = web::scope("/"); + // Generate routes for mini-dashboard assets + for (path, resource) in generated.into_iter() { + let Resource { + mime_type, data, .. + } = resource; + // Redirect index.html to / + if path == "index.html" { + config.service(web::resource("/").route( + web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)), + )); + } else { + scope = scope.service(web::resource(path).route( + web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)), + )); } - config.service(scope); + } + config.service(scope); } else { - config.service(routes::running); + config.service(web::resource("/").route(web::get().to(routes::running))); } } #[cfg(not(feature = "mini-dashboard"))] pub fn dashboard(config: &mut web::ServiceConfig, _enable_frontend: bool) { - config.service(routes::running); + config.service(web::resource("/").route(web::get().to(routes::running))); } #[macro_export] @@ -80,10 +107,11 @@ macro_rules! create_app { use actix_web::App; use actix_web::{middleware, web}; use meilisearch_http::routes::*; - use meilisearch_http::{configure_data, dashboard}; + use meilisearch_http::{configure_auth, configure_data, dashboard}; App::new() .configure(|s| configure_data(s, $data.clone())) + .configure(|s| configure_auth(s, &$data)) .configure(document::services) .configure(index::services) .configure(search::services) diff --git a/meilisearch-http/src/routes/document.rs b/meilisearch-http/src/routes/document.rs index 6ac521f79..817f624d0 100644 --- a/meilisearch-http/src/routes/document.rs +++ b/meilisearch-http/src/routes/document.rs @@ -1,14 +1,12 @@ -use actix_web::{delete, get, post, put}; use actix_web::{web, HttpResponse}; -use indexmap::IndexMap; -use log::{debug, error}; +use log::debug; use milli::update::{IndexDocumentsMethod, UpdateFormat}; use serde::Deserialize; use serde_json::Value; use crate::error::ResponseError; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::payload::Payload; -use crate::helpers::Authentication; use crate::routes::IndexParam; use crate::Data; @@ -17,7 +15,6 @@ const DEFAULT_RETRIEVE_DOCUMENTS_LIMIT: usize = 20; macro_rules! guard_content_type { ($fn_name:ident, $guard_value:literal) => { - #[allow(dead_code)] fn $fn_name(head: &actix_web::dev::RequestHead) -> bool { if let Some(content_type) = head.headers.get("Content-Type") { content_type @@ -33,8 +30,6 @@ macro_rules! guard_content_type { guard_content_type!(guard_json, "application/json"); -type Document = IndexMap; - #[derive(Deserialize)] struct DocumentParam { index_uid: String, @@ -42,21 +37,27 @@ struct DocumentParam { } pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(get_document) - .service(delete_document) - .service(get_all_documents) - .service(add_documents) - .service(update_documents) - .service(delete_documents) - .service(clear_all_documents); + cfg.service( + web::scope("/indexes/{index_uid}/documents") + .service( + web::resource("") + .route(web::get().to(get_all_documents)) + .route(web::post().guard(guard_json).to(add_documents)) + .route(web::put().guard(guard_json).to(update_documents)) + .route(web::delete().to(clear_all_documents)), + ) + // this route needs to be before the /documents/{document_id} to match properly + .service(web::resource("/delete-batch").route(web::post().to(delete_documents))) + .service( + web::resource("/{document_id}") + .route(web::get().to(get_document)) + .route(web::delete().to(delete_document)), + ), + ); } -#[get( - "/indexes/{index_uid}/documents/{document_id}", - wrap = "Authentication::Public" -)] async fn get_document( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let index = path.index_uid.clone(); @@ -68,12 +69,8 @@ async fn get_document( Ok(HttpResponse::Ok().json(document)) } -#[delete( - "/indexes/{index_uid}/documents/{document_id}", - wrap = "Authentication::Private" -)] async fn delete_document( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let update_status = data @@ -91,9 +88,8 @@ struct BrowseQuery { attributes_to_retrieve: Option, } -#[get("/indexes/{index_uid}/documents", wrap = "Authentication::Public")] async fn get_all_documents( - data: web::Data, + data: GuardedData, path: web::Path, params: web::Query, ) -> Result { @@ -129,9 +125,8 @@ struct UpdateDocumentsQuery { /// Route used when the payload type is "application/json" /// Used to add or replace documents -#[post("/indexes/{index_uid}/documents", wrap = "Authentication::Private")] async fn add_documents( - data: web::Data, + data: GuardedData, path: web::Path, params: web::Query, body: Payload, @@ -151,33 +146,8 @@ async fn add_documents( Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) } -/// Default route for adding documents, this should return an error and redirect to the documentation -#[post("/indexes/{index_uid}/documents", wrap = "Authentication::Private")] -async fn add_documents_default( - _data: web::Data, - _path: web::Path, - _params: web::Query, - _body: web::Json>, -) -> Result { - error!("Unknown document type"); - todo!() -} - -/// Default route for adding documents, this should return an error and redirect to the documentation -#[put("/indexes/{index_uid}/documents", wrap = "Authentication::Private")] -async fn update_documents_default( - _data: web::Data, - _path: web::Path, - _params: web::Query, - _body: web::Json>, -) -> Result { - error!("Unknown document type"); - todo!() -} - -#[put("/indexes/{index_uid}/documents", wrap = "Authentication::Private")] async fn update_documents( - data: web::Data, + data: GuardedData, path: web::Path, params: web::Query, body: Payload, @@ -197,12 +167,8 @@ async fn update_documents( Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update.id() }))) } -#[post( - "/indexes/{index_uid}/documents/delete-batch", - wrap = "Authentication::Private" -)] async fn delete_documents( - data: web::Data, + data: GuardedData, path: web::Path, body: web::Json>, ) -> Result { @@ -221,10 +187,8 @@ async fn delete_documents( Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) } -/// delete all documents -#[delete("/indexes/{index_uid}/documents", wrap = "Authentication::Private")] async fn clear_all_documents( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let update_status = data.clear_documents(path.index_uid.clone()).await?; diff --git a/meilisearch-http/src/routes/dump.rs b/meilisearch-http/src/routes/dump.rs index f905207ec..e506755a1 100644 --- a/meilisearch-http/src/routes/dump.rs +++ b/meilisearch-http/src/routes/dump.rs @@ -1,18 +1,17 @@ -use actix_web::HttpResponse; -use actix_web::{get, post, web}; use log::debug; +use actix_web::{web, HttpResponse}; use serde::{Deserialize, Serialize}; use crate::error::ResponseError; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::Data; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(create_dump).service(get_dump_status); + cfg.service(web::resource("/dumps").route(web::post().to(create_dump))) + .service(web::resource("/dumps/{dump_uid}/status").route(web::get().to(get_dump_status))); } -#[post("/dumps", wrap = "Authentication::Private")] -async fn create_dump(data: web::Data) -> Result { +async fn create_dump(data: GuardedData) -> Result { let res = data.create_dump().await?; debug!("returns: {:?}", res); @@ -30,9 +29,8 @@ struct DumpParam { dump_uid: String, } -#[get("/dumps/{dump_uid}/status", wrap = "Authentication::Private")] async fn get_dump_status( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let res = data.dump_status(path.dump_uid.clone()).await?; diff --git a/meilisearch-http/src/routes/health.rs b/meilisearch-http/src/routes/health.rs index 8994df722..54237de1a 100644 --- a/meilisearch-http/src/routes/health.rs +++ b/meilisearch-http/src/routes/health.rs @@ -1,13 +1,11 @@ -use actix_web::get; use actix_web::{web, HttpResponse}; use crate::error::ResponseError; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(get_health); + cfg.service(web::resource("/health").route(web::get().to(get_health))); } -#[get("/health")] async fn get_health() -> Result { Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" }))) } diff --git a/meilisearch-http/src/routes/index.rs b/meilisearch-http/src/routes/index.rs index 0f29c985e..eb8da92ed 100644 --- a/meilisearch-http/src/routes/index.rs +++ b/meilisearch-http/src/routes/index.rs @@ -1,4 +1,3 @@ -use actix_web::{delete, get, post, put}; use actix_web::{web, HttpResponse}; use chrono::{DateTime, Utc}; use log::debug; @@ -6,34 +5,29 @@ use serde::{Deserialize, Serialize}; use super::{IndexParam, UpdateStatusResponse}; use crate::error::ResponseError; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::Data; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(list_indexes) - .service(get_index) - .service(create_index) - .service(update_index) - .service(delete_index) - .service(get_update_status) - .service(get_all_updates_status); -} - -#[get("/indexes", wrap = "Authentication::Private")] -async fn list_indexes(data: web::Data) -> Result { - let indexes = data.list_indexes().await?; - debug!("returns: {:?}", indexes); - Ok(HttpResponse::Ok().json(indexes)) -} - -#[get("/indexes/{index_uid}", wrap = "Authentication::Private")] -async fn get_index( - data: web::Data, - path: web::Path, -) -> Result { - let meta = data.index(path.index_uid.clone()).await?; - debug!("returns: {:?}", meta); - Ok(HttpResponse::Ok().json(meta)) + cfg.service( + web::resource("indexes") + .route(web::get().to(list_indexes)) + .route(web::post().to(create_index)), + ) + .service( + web::resource("/indexes/{index_uid}") + .route(web::get().to(get_index)) + .route(web::put().to(update_index)) + .route(web::delete().to(delete_index)), + ) + .service( + web::resource("/indexes/{index_uid}/updates") + .route(web::get().to(get_all_updates_status)) + ) + .service( + web::resource("/indexes/{index_uid}/updates/{update_id}") + .route(web::get().to(get_update_status)) + ); } #[derive(Debug, Deserialize)] @@ -43,18 +37,6 @@ struct IndexCreateRequest { primary_key: Option, } -#[post("/indexes", wrap = "Authentication::Private")] -async fn create_index( - data: web::Data, - body: web::Json, -) -> Result { - debug!("called with params: {:?}", body); - let body = body.into_inner(); - let meta = data.create_index(body.uid, body.primary_key).await?; - debug!("returns: {:?}", meta); - Ok(HttpResponse::Ok().json(meta)) -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct UpdateIndexRequest { @@ -72,9 +54,32 @@ pub struct UpdateIndexResponse { primary_key: Option, } -#[put("/indexes/{index_uid}", wrap = "Authentication::Private")] +async fn list_indexes(data: GuardedData) -> Result { + let indexes = data.list_indexes().await?; + debug!("returns: {:?}", indexes); + Ok(HttpResponse::Ok().json(indexes)) +} + +async fn create_index( + data: GuardedData, + body: web::Json, +) -> Result { + let body = body.into_inner(); + let meta = data.create_index(body.uid, body.primary_key).await?; + Ok(HttpResponse::Ok().json(meta)) +} + +async fn get_index( + data: GuardedData, + path: web::Path, +) -> Result { + let meta = data.index(path.index_uid.clone()).await?; + debug!("returns: {:?}", meta); + Ok(HttpResponse::Ok().json(meta)) +} + async fn update_index( - data: web::Data, + data: GuardedData, path: web::Path, body: web::Json, ) -> Result { @@ -87,9 +92,8 @@ async fn update_index( Ok(HttpResponse::Ok().json(meta)) } -#[delete("/indexes/{index_uid}", wrap = "Authentication::Private")] async fn delete_index( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { data.delete_index(path.index_uid.clone()).await?; @@ -102,12 +106,8 @@ struct UpdateParam { update_id: u64, } -#[get( - "/indexes/{index_uid}/updates/{update_id}", - wrap = "Authentication::Private" -)] async fn get_update_status( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let params = path.into_inner(); @@ -119,9 +119,8 @@ async fn get_update_status( Ok(HttpResponse::Ok().json(meta)) } -#[get("/indexes/{index_uid}/updates", wrap = "Authentication::Private")] async fn get_all_updates_status( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let metas = data.get_updates_status(path.into_inner().index_uid).await?; diff --git a/meilisearch-http/src/routes/key.rs b/meilisearch-http/src/routes/key.rs index b44d747c8..d47e264be 100644 --- a/meilisearch-http/src/routes/key.rs +++ b/meilisearch-http/src/routes/key.rs @@ -1,13 +1,11 @@ -use actix_web::get; -use actix_web::web; -use actix_web::HttpResponse; +use actix_web::{web, HttpResponse}; use serde::Serialize; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::Data; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(list); + cfg.service(web::resource("/keys").route(web::get().to(list))); } #[derive(Serialize)] @@ -16,8 +14,7 @@ struct KeysResponse { public: Option, } -#[get("/keys", wrap = "Authentication::Admin")] -async fn list(data: web::Data) -> HttpResponse { +async fn list(data: GuardedData) -> HttpResponse { let api_keys = data.api_keys.clone(); HttpResponse::Ok().json(&KeysResponse { private: api_keys.private, diff --git a/meilisearch-http/src/routes/mod.rs b/meilisearch-http/src/routes/mod.rs index f4581ebcb..520949cd8 100644 --- a/meilisearch-http/src/routes/mod.rs +++ b/meilisearch-http/src/routes/mod.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use actix_web::{get, HttpResponse}; +use actix_web::HttpResponse; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -220,7 +220,6 @@ impl IndexUpdateResponse { /// "status": "Meilisearch is running" /// } /// ``` -#[get("/")] pub async fn running() -> HttpResponse { HttpResponse::Ok().json(serde_json::json!({ "status": "MeiliSearch is running" })) } diff --git a/meilisearch-http/src/routes/search.rs b/meilisearch-http/src/routes/search.rs index cc1306e71..7307a5990 100644 --- a/meilisearch-http/src/routes/search.rs +++ b/meilisearch-http/src/routes/search.rs @@ -1,18 +1,22 @@ use std::collections::{BTreeSet, HashSet}; -use actix_web::{get, post, web, HttpResponse}; use log::debug; +use actix_web::{web, HttpResponse}; use serde::Deserialize; use serde_json::Value; use crate::error::ResponseError; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::index::{default_crop_length, SearchQuery, DEFAULT_SEARCH_LIMIT}; use crate::routes::IndexParam; use crate::Data; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(search_with_post).service(search_with_url_query); + cfg.service( + web::resource("/indexes/{index_uid}/search") + .route(web::get().to(search_with_url_query)) + .route(web::post().to(search_with_post)), + ); } #[derive(Deserialize, Debug)] @@ -73,9 +77,8 @@ impl From for SearchQuery { } } -#[get("/indexes/{index_uid}/search", wrap = "Authentication::Public")] async fn search_with_url_query( - data: web::Data, + data: GuardedData, path: web::Path, params: web::Query, ) -> Result { @@ -86,9 +89,8 @@ async fn search_with_url_query( Ok(HttpResponse::Ok().json(search_result)) } -#[post("/indexes/{index_uid}/search", wrap = "Authentication::Public")] async fn search_with_post( - data: web::Data, + data: GuardedData, path: web::Path, params: web::Json, ) -> Result { diff --git a/meilisearch-http/src/routes/settings.rs b/meilisearch-http/src/routes/settings.rs index 5e4ecf1a4..6a8d9cca5 100644 --- a/meilisearch-http/src/routes/settings.rs +++ b/meilisearch-http/src/routes/settings.rs @@ -1,7 +1,7 @@ -use actix_web::{delete, get, post, web, HttpResponse}; use log::debug; +use actix_web::{web, HttpResponse}; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::index::Settings; use crate::Data; use crate::{error::ResponseError, index::Unchecked}; @@ -11,16 +11,15 @@ macro_rules! make_setting_route { ($route:literal, $type:ty, $attr:ident, $camelcase_attr:literal) => { mod $attr { use log::debug; - use actix_web::{web, HttpResponse}; + use actix_web::{web, HttpResponse, Resource}; use crate::data; use crate::error::ResponseError; - use crate::helpers::Authentication; use crate::index::Settings; + use crate::extractors::authentication::{GuardedData, policies::*}; - #[actix_web::delete($route, wrap = "Authentication::Private")] - pub async fn delete( - data: web::Data, + async fn delete( + data: GuardedData, index_uid: web::Path, ) -> Result { use crate::index::Settings; @@ -33,9 +32,8 @@ macro_rules! make_setting_route { Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) } - #[actix_web::post($route, wrap = "Authentication::Private")] - pub async fn update( - data: actix_web::web::Data, + async fn update( + data: GuardedData, index_uid: actix_web::web::Path, body: actix_web::web::Json>, ) -> std::result::Result { @@ -49,9 +47,8 @@ macro_rules! make_setting_route { Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) } - #[actix_web::get($route, wrap = "Authentication::Private")] - pub async fn get( - data: actix_web::web::Data, + async fn get( + data: GuardedData, index_uid: actix_web::web::Path, ) -> std::result::Result { let settings = data.settings(index_uid.into_inner()).await?; @@ -60,6 +57,13 @@ macro_rules! make_setting_route { let val = json[$camelcase_attr].take(); Ok(HttpResponse::Ok().json(val)) } + + pub fn resources() -> Resource { + Resource::new($route) + .route(web::get().to(get)) + .route(web::post().to(update)) + .route(web::delete().to(delete)) + } } }; } @@ -117,14 +121,11 @@ macro_rules! create_services { ($($mod:ident),*) => { pub fn services(cfg: &mut web::ServiceConfig) { cfg - .service(update_all) - .service(get_all) - .service(delete_all) - $( - .service($mod::get) - .service($mod::update) - .service($mod::delete) - )*; + .service(web::resource("/indexes/{index_uid}/settings") + .route(web::post().to(update_all)) + .route(web::get().to(get_all)) + .route(web::delete().to(delete_all))) + $(.service($mod::resources()))*; } }; } @@ -139,9 +140,8 @@ create_services!( ranking_rules ); -#[post("/indexes/{index_uid}/settings", wrap = "Authentication::Private")] async fn update_all( - data: web::Data, + data: GuardedData, index_uid: web::Path, body: web::Json>, ) -> Result { @@ -154,9 +154,8 @@ async fn update_all( Ok(HttpResponse::Accepted().json(json)) } -#[get("/indexes/{index_uid}/settings", wrap = "Authentication::Private")] async fn get_all( - data: web::Data, + data: GuardedData, index_uid: web::Path, ) -> Result { let settings = data.settings(index_uid.into_inner()).await?; @@ -164,9 +163,8 @@ async fn get_all( Ok(HttpResponse::Ok().json(settings)) } -#[delete("/indexes/{index_uid}/settings", wrap = "Authentication::Private")] async fn delete_all( - data: web::Data, + data: GuardedData, index_uid: web::Path, ) -> Result { let settings = Settings::cleared(); diff --git a/meilisearch-http/src/routes/stats.rs b/meilisearch-http/src/routes/stats.rs index c39989188..e440ce8ff 100644 --- a/meilisearch-http/src/routes/stats.rs +++ b/meilisearch-http/src/routes/stats.rs @@ -1,23 +1,20 @@ -use actix_web::get; -use actix_web::web; -use actix_web::HttpResponse; use log::debug; +use actix_web::{web, HttpResponse}; use serde::Serialize; use crate::error::ResponseError; -use crate::helpers::Authentication; +use crate::extractors::authentication::{policies::*, GuardedData}; use crate::routes::IndexParam; use crate::Data; pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(get_index_stats) - .service(get_stats) - .service(get_version); + cfg.service(web::resource("/indexes/{index_uid}/stats").route(web::get().to(get_index_stats))) + .service(web::resource("/stats").route(web::get().to(get_stats))) + .service(web::resource("/version").route(web::get().to(get_version))); } -#[get("/indexes/{index_uid}/stats", wrap = "Authentication::Private")] async fn get_index_stats( - data: web::Data, + data: GuardedData, path: web::Path, ) -> Result { let response = data.get_index_stats(path.index_uid.clone()).await?; @@ -26,8 +23,7 @@ async fn get_index_stats( Ok(HttpResponse::Ok().json(response)) } -#[get("/stats", wrap = "Authentication::Private")] -async fn get_stats(data: web::Data) -> Result { +async fn get_stats(data: GuardedData) -> Result { let response = data.get_all_stats().await?; debug!("returns: {:?}", response); @@ -42,8 +38,7 @@ struct VersionResponse { pkg_version: String, } -#[get("/version", wrap = "Authentication::Private")] -async fn get_version() -> HttpResponse { +async fn get_version(_data: GuardedData) -> HttpResponse { let commit_sha = match option_env!("COMMIT_SHA") { Some("") | None => env!("VERGEN_SHA"), Some(commit_sha) => commit_sha, diff --git a/meilisearch-http/tests/documents/delete_documents.rs b/meilisearch-http/tests/documents/delete_documents.rs index d5794e40c..eb6fa040b 100644 --- a/meilisearch-http/tests/documents/delete_documents.rs +++ b/meilisearch-http/tests/documents/delete_documents.rs @@ -14,8 +14,8 @@ async fn delete_one_unexisting_document() { let server = Server::new().await; let index = server.index("test"); index.create(None).await; - let (_response, code) = index.delete_document(0).await; - assert_eq!(code, 202); + let (response, code) = index.delete_document(0).await; + assert_eq!(code, 202, "{}", response); let update = index.wait_update_id(0).await; assert_eq!(update["status"], "processed"); } @@ -85,8 +85,8 @@ async fn clear_all_documents_empty_index() { #[actix_rt::test] async fn delete_batch_unexisting_index() { let server = Server::new().await; - let (_response, code) = server.index("test").delete_batch(vec![]).await; - assert_eq!(code, 404); + let (response, code) = server.index("test").delete_batch(vec![]).await; + assert_eq!(code, 404, "{}", response); } #[actix_rt::test] diff --git a/meilisearch-http/tests/settings/get_settings.rs b/meilisearch-http/tests/settings/get_settings.rs index 4941b5d2f..0b523eef3 100644 --- a/meilisearch-http/tests/settings/get_settings.rs +++ b/meilisearch-http/tests/settings/get_settings.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use once_cell::sync::Lazy; -use serde_json::{Value, json}; +use serde_json::{json, Value}; use crate::common::Server; @@ -11,7 +11,10 @@ static DEFAULT_SETTINGS_VALUES: Lazy> = Lazy::new(| map.insert("searchable_attributes", json!(["*"])); map.insert("filterable_attributes", json!([])); map.insert("distinct_attribute", json!(Value::Null)); - map.insert("ranking_rules", json!(["words", "typo", "proximity", "attribute", "exactness"])); + map.insert( + "ranking_rules", + json!(["words", "typo", "proximity", "attribute", "exactness"]), + ); map.insert("stop_words", json!([])); map.insert("synonyms", json!({})); map