228: Authentication rework r=curquiza a=MarinPostma

In an attempt to fix #201, I ended up rewriting completely the authentication system we use. This is because actix doesn't allow to wrap a single route into a middleware, so we initially put each route into it's own service to use the authentication middleware. Routes are now grouped in resources, fixing #201.

As for the authentication, I decided to take a very different approach, and ditch middleware altogether. Instead, I decided to use actix's [extractor](https://actix.rs/docs/extractors/). `Data` is now wrapped in a `GuardedData<P: Policy, T>` (where `T` is `Data`) in each route. The `Policy` trait, thanks to the `authenticate` method tell if a request is authorized to access the resources in the route. Concretely, before the server starts, it is configured with a `AuthConfig` instance that can either be `AuthConfig::NoAuth` when no auth is required at runtime, or `AuthConfig::Auth(Policies)`, where `Policies` maps the `Policy` type to it singleton instance.

In the current implementation, and this to match the legacy meilisearch behaviour, each policy implementation contains a `HashSet` of token (`Vec<u8>` for now), that represents the user it can authenticate. When starting the program, each key (identified as a user) is given a set of `Policy`, representing its roles. The later is facilitated by the `create_users` macro, like so:

```rust
create_users!(
    policies,
    master_key.as_bytes() => { Admin, Private, Public },
    private_key.as_bytes() => { Private, Public },
    public_key.as_bytes() => { Public }
);
```

This is some groundwork for later development on a full fledged authentication system for meilisearch.


fix #201

Co-authored-by: marin postma <postma.marin@protonmail.com>
This commit is contained in:
bors[bot] 2021-06-28 08:38:59 +00:00 committed by GitHub
commit d7ca68d8e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 395 additions and 373 deletions

View File

@ -10,23 +10,6 @@ use meilisearch_error::{Code, ErrorCode};
use milli::UserError; use milli::UserError;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ResponseError { pub struct ResponseError {

View File

@ -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,
}
}
}

View File

@ -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<Vec<u8>>
}
impl $name {
pub fn new() -> Self {
Self { inner: HashSet::new() }
}
pub fn add(&mut self, token: Vec<u8>) {
&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<T, D> {
data: D,
_marker: PhantomData<T>,
}
impl<T, D> Deref for GuardedData<T, D> {
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<TypeId, Box<dyn Any>>,
}
impl Policies {
pub fn new() -> Self {
Self {
inner: HashMap::new(),
}
}
pub fn insert<S: Policy + 'static>(&mut self, policy: S) {
self.inner.insert(TypeId::of::<S>(), Box::new(policy));
}
pub fn get<S: Policy + 'static>(&self) -> Option<&S> {
self.inner
.get(&TypeId::of::<S>())
.and_then(|p| p.downcast_ref::<S>())
}
pub fn get_mut<S: Policy + 'static>(&mut self) -> Option<&mut S> {
self.inner
.get_mut(&TypeId::of::<S>())
.and_then(|p| p.downcast_mut::<S>())
}
}
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<P: Policy + 'static, D: 'static + Clone> FromRequest for GuardedData<P, D> {
type Config = AuthConfig;
type Error = ResponseError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(
req: &actix_web::HttpRequest,
_payload: &mut actix_http::Payload,
) -> Self::Future {
match req.app_data::<Self::Config>() {
Some(config) => match config {
AuthConfig::NoAuth => match req.app_data::<D>().cloned() {
Some(data) => ok(Self {
data,
_marker: PhantomData,
}),
None => err(AuthenticationError::IrretrievableState.into()),
},
AuthConfig::Auth(policies) => match policies.get::<P>() {
Some(policy) => match req.headers().get("x-meili-api-key") {
Some(token) => {
if policy.authenticate(token.as_bytes()) {
match req.app_data::<D>().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()),
}
}
}

View File

@ -1 +1,3 @@
pub mod payload; pub mod payload;
#[macro_use]
pub mod authentication;

View File

@ -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<S: 'static> Transform<S, ServiceRequest> for Authentication
where
S: Service<ServiceRequest, Response = ServiceResponse<Body>, Error = actix_web::Error>,
{
type Response = ServiceResponse<Body>;
type Error = actix_web::Error;
type InitError = ();
type Transform = LoggingMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(LoggingMiddleware {
acl: *self,
service,
})
}
}
pub struct LoggingMiddleware<S> {
acl: Authentication,
service: S,
}
#[allow(clippy::type_complexity)]
impl<S> Service<ServiceRequest> for LoggingMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<Body>, Error = actix_web::Error>,
{
type Response = ServiceResponse<Body>;
type Error = actix_web::Error;
type Future = AuthenticationFuture<S>;
fn poll_ready(&self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let data = req.app_data::<web::Data<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<S>
where
S: Service<ServiceRequest>,
{
Authenticated(#[pin] S::Future),
NoHeader(Option<ServiceRequest>),
Refused(Option<ServiceRequest>),
}
impl<S> Future for AuthenticationFuture<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<Body>, Error = actix_web::Error>,
{
type Output = Result<ServiceResponse<Body>, actix_web::Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
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"),
}
}
}
}
}

View File

@ -1,6 +1,4 @@
pub mod authentication;
pub mod compression; pub mod compression;
mod env; mod env;
pub use authentication::Authentication;
pub use env::EnvSizer; pub use env::EnvSizer;

View File

@ -1,6 +1,7 @@
pub mod data; pub mod data;
#[macro_use] #[macro_use]
pub mod error; pub mod error;
#[macro_use]
pub mod extractors; pub mod extractors;
pub mod helpers; pub mod helpers;
mod index; mod index;
@ -11,17 +12,21 @@ pub mod routes;
#[cfg(all(not(debug_assertions), feature = "analytics"))] #[cfg(all(not(debug_assertions), feature = "analytics"))]
pub mod analytics; pub mod analytics;
use crate::extractors::authentication::AuthConfig;
pub use self::data::Data; pub use self::data::Data;
pub use option::Opt; pub use option::Opt;
use actix_web::web; use actix_web::web;
use extractors::authentication::policies::*;
use extractors::payload::PayloadConfig; use extractors::payload::PayloadConfig;
pub fn configure_data(config: &mut web::ServiceConfig, data: Data) { pub fn configure_data(config: &mut web::ServiceConfig, data: Data) {
let http_payload_size_limit = data.http_payload_size_limit(); let http_payload_size_limit = data.http_payload_size_limit();
config config
.data(data) .data(data.clone())
.app_data(data)
.app_data( .app_data(
web::JsonConfig::default() web::JsonConfig::default()
.limit(http_payload_size_limit) .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")] #[cfg(feature = "mini-dashboard")]
pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) { pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) {
use actix_web_static_files::Resource;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use actix_web_static_files::Resource;
mod generated { mod generated {
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
@ -49,27 +74,29 @@ pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) {
let mut scope = web::scope("/"); let mut scope = web::scope("/");
// Generate routes for mini-dashboard assets // Generate routes for mini-dashboard assets
for (path, resource) in generated.into_iter() { for (path, resource) in generated.into_iter() {
let Resource {mime_type, data, ..} = resource; let Resource {
mime_type, data, ..
} = resource;
// Redirect index.html to / // Redirect index.html to /
if path == "index.html" { if path == "index.html" {
config.service(web::resource("/").route(web::get().to(move || { config.service(web::resource("/").route(
HttpResponse::Ok().content_type(mime_type).body(data) web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)),
}))); ));
} else { } else {
scope = scope.service(web::resource(path).route(web::get().to(move || { scope = scope.service(web::resource(path).route(
HttpResponse::Ok().content_type(mime_type).body(data) web::get().to(move || HttpResponse::Ok().content_type(mime_type).body(data)),
}))); ));
} }
} }
config.service(scope); config.service(scope);
} else { } else {
config.service(routes::running); config.service(web::resource("/").route(web::get().to(routes::running)));
} }
} }
#[cfg(not(feature = "mini-dashboard"))] #[cfg(not(feature = "mini-dashboard"))]
pub fn dashboard(config: &mut web::ServiceConfig, _enable_frontend: bool) { 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] #[macro_export]
@ -80,10 +107,11 @@ macro_rules! create_app {
use actix_web::App; use actix_web::App;
use actix_web::{middleware, web}; use actix_web::{middleware, web};
use meilisearch_http::routes::*; use meilisearch_http::routes::*;
use meilisearch_http::{configure_data, dashboard}; use meilisearch_http::{configure_auth, configure_data, dashboard};
App::new() App::new()
.configure(|s| configure_data(s, $data.clone())) .configure(|s| configure_data(s, $data.clone()))
.configure(|s| configure_auth(s, &$data))
.configure(document::services) .configure(document::services)
.configure(index::services) .configure(index::services)
.configure(search::services) .configure(search::services)

View File

@ -1,14 +1,12 @@
use actix_web::{delete, get, post, put};
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use indexmap::IndexMap; use log::debug;
use log::{debug, error};
use milli::update::{IndexDocumentsMethod, UpdateFormat}; use milli::update::{IndexDocumentsMethod, UpdateFormat};
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use crate::error::ResponseError; use crate::error::ResponseError;
use crate::extractors::authentication::{policies::*, GuardedData};
use crate::extractors::payload::Payload; use crate::extractors::payload::Payload;
use crate::helpers::Authentication;
use crate::routes::IndexParam; use crate::routes::IndexParam;
use crate::Data; use crate::Data;
@ -17,7 +15,6 @@ const DEFAULT_RETRIEVE_DOCUMENTS_LIMIT: usize = 20;
macro_rules! guard_content_type { macro_rules! guard_content_type {
($fn_name:ident, $guard_value:literal) => { ($fn_name:ident, $guard_value:literal) => {
#[allow(dead_code)]
fn $fn_name(head: &actix_web::dev::RequestHead) -> bool { fn $fn_name(head: &actix_web::dev::RequestHead) -> bool {
if let Some(content_type) = head.headers.get("Content-Type") { if let Some(content_type) = head.headers.get("Content-Type") {
content_type content_type
@ -33,8 +30,6 @@ macro_rules! guard_content_type {
guard_content_type!(guard_json, "application/json"); guard_content_type!(guard_json, "application/json");
type Document = IndexMap<String, Value>;
#[derive(Deserialize)] #[derive(Deserialize)]
struct DocumentParam { struct DocumentParam {
index_uid: String, index_uid: String,
@ -42,21 +37,27 @@ struct DocumentParam {
} }
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_document) cfg.service(
.service(delete_document) web::scope("/indexes/{index_uid}/documents")
.service(get_all_documents) .service(
.service(add_documents) web::resource("")
.service(update_documents) .route(web::get().to(get_all_documents))
.service(delete_documents) .route(web::post().guard(guard_json).to(add_documents))
.service(clear_all_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( async fn get_document(
data: web::Data<Data>, data: GuardedData<Public, Data>,
path: web::Path<DocumentParam>, path: web::Path<DocumentParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index = path.index_uid.clone(); let index = path.index_uid.clone();
@ -68,12 +69,8 @@ async fn get_document(
Ok(HttpResponse::Ok().json(document)) Ok(HttpResponse::Ok().json(document))
} }
#[delete(
"/indexes/{index_uid}/documents/{document_id}",
wrap = "Authentication::Private"
)]
async fn delete_document( async fn delete_document(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<DocumentParam>, path: web::Path<DocumentParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let update_status = data let update_status = data
@ -91,9 +88,8 @@ struct BrowseQuery {
attributes_to_retrieve: Option<String>, attributes_to_retrieve: Option<String>,
} }
#[get("/indexes/{index_uid}/documents", wrap = "Authentication::Public")]
async fn get_all_documents( async fn get_all_documents(
data: web::Data<Data>, data: GuardedData<Public, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
params: web::Query<BrowseQuery>, params: web::Query<BrowseQuery>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -129,9 +125,8 @@ struct UpdateDocumentsQuery {
/// Route used when the payload type is "application/json" /// Route used when the payload type is "application/json"
/// Used to add or replace documents /// Used to add or replace documents
#[post("/indexes/{index_uid}/documents", wrap = "Authentication::Private")]
async fn add_documents( async fn add_documents(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
params: web::Query<UpdateDocumentsQuery>, params: web::Query<UpdateDocumentsQuery>,
body: Payload, body: Payload,
@ -151,33 +146,8 @@ async fn add_documents(
Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) 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<Data>,
_path: web::Path<IndexParam>,
_params: web::Query<UpdateDocumentsQuery>,
_body: web::Json<Vec<Document>>,
) -> Result<HttpResponse, ResponseError> {
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<Data>,
_path: web::Path<IndexParam>,
_params: web::Query<UpdateDocumentsQuery>,
_body: web::Json<Vec<Document>>,
) -> Result<HttpResponse, ResponseError> {
error!("Unknown document type");
todo!()
}
#[put("/indexes/{index_uid}/documents", wrap = "Authentication::Private")]
async fn update_documents( async fn update_documents(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
params: web::Query<UpdateDocumentsQuery>, params: web::Query<UpdateDocumentsQuery>,
body: Payload, body: Payload,
@ -197,12 +167,8 @@ async fn update_documents(
Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update.id() }))) Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update.id() })))
} }
#[post(
"/indexes/{index_uid}/documents/delete-batch",
wrap = "Authentication::Private"
)]
async fn delete_documents( async fn delete_documents(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
body: web::Json<Vec<Value>>, body: web::Json<Vec<Value>>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -221,10 +187,8 @@ async fn delete_documents(
Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) 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( async fn clear_all_documents(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let update_status = data.clear_documents(path.index_uid.clone()).await?; let update_status = data.clear_documents(path.index_uid.clone()).await?;

View File

@ -1,18 +1,17 @@
use actix_web::HttpResponse;
use actix_web::{get, post, web};
use log::debug; use log::debug;
use actix_web::{web, HttpResponse};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::ResponseError; use crate::error::ResponseError;
use crate::helpers::Authentication; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::Data; use crate::Data;
pub fn services(cfg: &mut web::ServiceConfig) { 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: GuardedData<Private, Data>) -> Result<HttpResponse, ResponseError> {
async fn create_dump(data: web::Data<Data>) -> Result<HttpResponse, ResponseError> {
let res = data.create_dump().await?; let res = data.create_dump().await?;
debug!("returns: {:?}", res); debug!("returns: {:?}", res);
@ -30,9 +29,8 @@ struct DumpParam {
dump_uid: String, dump_uid: String,
} }
#[get("/dumps/{dump_uid}/status", wrap = "Authentication::Private")]
async fn get_dump_status( async fn get_dump_status(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<DumpParam>, path: web::Path<DumpParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let res = data.dump_status(path.dump_uid.clone()).await?; let res = data.dump_status(path.dump_uid.clone()).await?;

View File

@ -1,13 +1,11 @@
use actix_web::get;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use crate::error::ResponseError; use crate::error::ResponseError;
pub fn services(cfg: &mut web::ServiceConfig) { 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<HttpResponse, ResponseError> { async fn get_health() -> Result<HttpResponse, ResponseError> {
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" }))) Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" })))
} }

View File

@ -1,4 +1,3 @@
use actix_web::{delete, get, post, put};
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use log::debug; use log::debug;
@ -6,34 +5,29 @@ use serde::{Deserialize, Serialize};
use super::{IndexParam, UpdateStatusResponse}; use super::{IndexParam, UpdateStatusResponse};
use crate::error::ResponseError; use crate::error::ResponseError;
use crate::helpers::Authentication; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::Data; use crate::Data;
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(list_indexes) cfg.service(
.service(get_index) web::resource("indexes")
.service(create_index) .route(web::get().to(list_indexes))
.service(update_index) .route(web::post().to(create_index)),
.service(delete_index) )
.service(get_update_status) .service(
.service(get_all_updates_status); web::resource("/indexes/{index_uid}")
} .route(web::get().to(get_index))
.route(web::put().to(update_index))
#[get("/indexes", wrap = "Authentication::Private")] .route(web::delete().to(delete_index)),
async fn list_indexes(data: web::Data<Data>) -> Result<HttpResponse, ResponseError> { )
let indexes = data.list_indexes().await?; .service(
debug!("returns: {:?}", indexes); web::resource("/indexes/{index_uid}/updates")
Ok(HttpResponse::Ok().json(indexes)) .route(web::get().to(get_all_updates_status))
} )
.service(
#[get("/indexes/{index_uid}", wrap = "Authentication::Private")] web::resource("/indexes/{index_uid}/updates/{update_id}")
async fn get_index( .route(web::get().to(get_update_status))
data: web::Data<Data>, );
path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> {
let meta = data.index(path.index_uid.clone()).await?;
debug!("returns: {:?}", meta);
Ok(HttpResponse::Ok().json(meta))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -43,18 +37,6 @@ struct IndexCreateRequest {
primary_key: Option<String>, primary_key: Option<String>,
} }
#[post("/indexes", wrap = "Authentication::Private")]
async fn create_index(
data: web::Data<Data>,
body: web::Json<IndexCreateRequest>,
) -> Result<HttpResponse, ResponseError> {
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)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
struct UpdateIndexRequest { struct UpdateIndexRequest {
@ -72,9 +54,32 @@ pub struct UpdateIndexResponse {
primary_key: Option<String>, primary_key: Option<String>,
} }
#[put("/indexes/{index_uid}", wrap = "Authentication::Private")] async fn list_indexes(data: GuardedData<Private, Data>) -> Result<HttpResponse, ResponseError> {
let indexes = data.list_indexes().await?;
debug!("returns: {:?}", indexes);
Ok(HttpResponse::Ok().json(indexes))
}
async fn create_index(
data: GuardedData<Private, Data>,
body: web::Json<IndexCreateRequest>,
) -> Result<HttpResponse, ResponseError> {
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<Private, Data>,
path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> {
let meta = data.index(path.index_uid.clone()).await?;
debug!("returns: {:?}", meta);
Ok(HttpResponse::Ok().json(meta))
}
async fn update_index( async fn update_index(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
body: web::Json<UpdateIndexRequest>, body: web::Json<UpdateIndexRequest>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -87,9 +92,8 @@ async fn update_index(
Ok(HttpResponse::Ok().json(meta)) Ok(HttpResponse::Ok().json(meta))
} }
#[delete("/indexes/{index_uid}", wrap = "Authentication::Private")]
async fn delete_index( async fn delete_index(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
data.delete_index(path.index_uid.clone()).await?; data.delete_index(path.index_uid.clone()).await?;
@ -102,12 +106,8 @@ struct UpdateParam {
update_id: u64, update_id: u64,
} }
#[get(
"/indexes/{index_uid}/updates/{update_id}",
wrap = "Authentication::Private"
)]
async fn get_update_status( async fn get_update_status(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<UpdateParam>, path: web::Path<UpdateParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let params = path.into_inner(); let params = path.into_inner();
@ -119,9 +119,8 @@ async fn get_update_status(
Ok(HttpResponse::Ok().json(meta)) Ok(HttpResponse::Ok().json(meta))
} }
#[get("/indexes/{index_uid}/updates", wrap = "Authentication::Private")]
async fn get_all_updates_status( async fn get_all_updates_status(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let metas = data.get_updates_status(path.into_inner().index_uid).await?; let metas = data.get_updates_status(path.into_inner().index_uid).await?;

View File

@ -1,13 +1,11 @@
use actix_web::get; use actix_web::{web, HttpResponse};
use actix_web::web;
use actix_web::HttpResponse;
use serde::Serialize; use serde::Serialize;
use crate::helpers::Authentication; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::Data; use crate::Data;
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(list); cfg.service(web::resource("/keys").route(web::get().to(list)));
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -16,8 +14,7 @@ struct KeysResponse {
public: Option<String>, public: Option<String>,
} }
#[get("/keys", wrap = "Authentication::Admin")] async fn list(data: GuardedData<Admin, Data>) -> HttpResponse {
async fn list(data: web::Data<Data>) -> HttpResponse {
let api_keys = data.api_keys.clone(); let api_keys = data.api_keys.clone();
HttpResponse::Ok().json(&KeysResponse { HttpResponse::Ok().json(&KeysResponse {
private: api_keys.private, private: api_keys.private,

View File

@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use actix_web::{get, HttpResponse}; use actix_web::HttpResponse;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -220,7 +220,6 @@ impl IndexUpdateResponse {
/// "status": "Meilisearch is running" /// "status": "Meilisearch is running"
/// } /// }
/// ``` /// ```
#[get("/")]
pub async fn running() -> HttpResponse { pub async fn running() -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({ "status": "MeiliSearch is running" })) HttpResponse::Ok().json(serde_json::json!({ "status": "MeiliSearch is running" }))
} }

View File

@ -1,18 +1,22 @@
use std::collections::{BTreeSet, HashSet}; use std::collections::{BTreeSet, HashSet};
use actix_web::{get, post, web, HttpResponse};
use log::debug; use log::debug;
use actix_web::{web, HttpResponse};
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use crate::error::ResponseError; 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::index::{default_crop_length, SearchQuery, DEFAULT_SEARCH_LIMIT};
use crate::routes::IndexParam; use crate::routes::IndexParam;
use crate::Data; use crate::Data;
pub fn services(cfg: &mut web::ServiceConfig) { 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)] #[derive(Deserialize, Debug)]
@ -73,9 +77,8 @@ impl From<SearchQueryGet> for SearchQuery {
} }
} }
#[get("/indexes/{index_uid}/search", wrap = "Authentication::Public")]
async fn search_with_url_query( async fn search_with_url_query(
data: web::Data<Data>, data: GuardedData<Admin, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
params: web::Query<SearchQueryGet>, params: web::Query<SearchQueryGet>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -86,9 +89,8 @@ async fn search_with_url_query(
Ok(HttpResponse::Ok().json(search_result)) Ok(HttpResponse::Ok().json(search_result))
} }
#[post("/indexes/{index_uid}/search", wrap = "Authentication::Public")]
async fn search_with_post( async fn search_with_post(
data: web::Data<Data>, data: GuardedData<Public, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
params: web::Json<SearchQuery>, params: web::Json<SearchQuery>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {

View File

@ -1,7 +1,7 @@
use actix_web::{delete, get, post, web, HttpResponse};
use log::debug; use log::debug;
use actix_web::{web, HttpResponse};
use crate::helpers::Authentication; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::index::Settings; use crate::index::Settings;
use crate::Data; use crate::Data;
use crate::{error::ResponseError, index::Unchecked}; use crate::{error::ResponseError, index::Unchecked};
@ -11,16 +11,15 @@ macro_rules! make_setting_route {
($route:literal, $type:ty, $attr:ident, $camelcase_attr:literal) => { ($route:literal, $type:ty, $attr:ident, $camelcase_attr:literal) => {
mod $attr { mod $attr {
use log::debug; use log::debug;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse, Resource};
use crate::data; use crate::data;
use crate::error::ResponseError; use crate::error::ResponseError;
use crate::helpers::Authentication;
use crate::index::Settings; use crate::index::Settings;
use crate::extractors::authentication::{GuardedData, policies::*};
#[actix_web::delete($route, wrap = "Authentication::Private")] async fn delete(
pub async fn delete( data: GuardedData<Private, data::Data>,
data: web::Data<data::Data>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
use crate::index::Settings; use crate::index::Settings;
@ -33,9 +32,8 @@ macro_rules! make_setting_route {
Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() })))
} }
#[actix_web::post($route, wrap = "Authentication::Private")] async fn update(
pub async fn update( data: GuardedData<Private, data::Data>,
data: actix_web::web::Data<data::Data>,
index_uid: actix_web::web::Path<String>, index_uid: actix_web::web::Path<String>,
body: actix_web::web::Json<Option<$type>>, body: actix_web::web::Json<Option<$type>>,
) -> std::result::Result<HttpResponse, ResponseError> { ) -> std::result::Result<HttpResponse, ResponseError> {
@ -49,9 +47,8 @@ macro_rules! make_setting_route {
Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() }))) Ok(HttpResponse::Accepted().json(serde_json::json!({ "updateId": update_status.id() })))
} }
#[actix_web::get($route, wrap = "Authentication::Private")] async fn get(
pub async fn get( data: GuardedData<Private, data::Data>,
data: actix_web::web::Data<data::Data>,
index_uid: actix_web::web::Path<String>, index_uid: actix_web::web::Path<String>,
) -> std::result::Result<HttpResponse, ResponseError> { ) -> std::result::Result<HttpResponse, ResponseError> {
let settings = data.settings(index_uid.into_inner()).await?; let settings = data.settings(index_uid.into_inner()).await?;
@ -60,6 +57,13 @@ macro_rules! make_setting_route {
let val = json[$camelcase_attr].take(); let val = json[$camelcase_attr].take();
Ok(HttpResponse::Ok().json(val)) 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),*) => { ($($mod:ident),*) => {
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
cfg cfg
.service(update_all) .service(web::resource("/indexes/{index_uid}/settings")
.service(get_all) .route(web::post().to(update_all))
.service(delete_all) .route(web::get().to(get_all))
$( .route(web::delete().to(delete_all)))
.service($mod::get) $(.service($mod::resources()))*;
.service($mod::update)
.service($mod::delete)
)*;
} }
}; };
} }
@ -139,9 +140,8 @@ create_services!(
ranking_rules ranking_rules
); );
#[post("/indexes/{index_uid}/settings", wrap = "Authentication::Private")]
async fn update_all( async fn update_all(
data: web::Data<Data>, data: GuardedData<Private, Data>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
body: web::Json<Settings<Unchecked>>, body: web::Json<Settings<Unchecked>>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -154,9 +154,8 @@ async fn update_all(
Ok(HttpResponse::Accepted().json(json)) Ok(HttpResponse::Accepted().json(json))
} }
#[get("/indexes/{index_uid}/settings", wrap = "Authentication::Private")]
async fn get_all( async fn get_all(
data: web::Data<Data>, data: GuardedData<Private, Data>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let settings = data.settings(index_uid.into_inner()).await?; let settings = data.settings(index_uid.into_inner()).await?;
@ -164,9 +163,8 @@ async fn get_all(
Ok(HttpResponse::Ok().json(settings)) Ok(HttpResponse::Ok().json(settings))
} }
#[delete("/indexes/{index_uid}/settings", wrap = "Authentication::Private")]
async fn delete_all( async fn delete_all(
data: web::Data<Data>, data: GuardedData<Private, Data>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let settings = Settings::cleared(); let settings = Settings::cleared();

View File

@ -1,23 +1,20 @@
use actix_web::get;
use actix_web::web;
use actix_web::HttpResponse;
use log::debug; use log::debug;
use actix_web::{web, HttpResponse};
use serde::Serialize; use serde::Serialize;
use crate::error::ResponseError; use crate::error::ResponseError;
use crate::helpers::Authentication; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::routes::IndexParam; use crate::routes::IndexParam;
use crate::Data; use crate::Data;
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_index_stats) cfg.service(web::resource("/indexes/{index_uid}/stats").route(web::get().to(get_index_stats)))
.service(get_stats) .service(web::resource("/stats").route(web::get().to(get_stats)))
.service(get_version); .service(web::resource("/version").route(web::get().to(get_version)));
} }
#[get("/indexes/{index_uid}/stats", wrap = "Authentication::Private")]
async fn get_index_stats( async fn get_index_stats(
data: web::Data<Data>, data: GuardedData<Private, Data>,
path: web::Path<IndexParam>, path: web::Path<IndexParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let response = data.get_index_stats(path.index_uid.clone()).await?; 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)) Ok(HttpResponse::Ok().json(response))
} }
#[get("/stats", wrap = "Authentication::Private")] async fn get_stats(data: GuardedData<Private, Data>) -> Result<HttpResponse, ResponseError> {
async fn get_stats(data: web::Data<Data>) -> Result<HttpResponse, ResponseError> {
let response = data.get_all_stats().await?; let response = data.get_all_stats().await?;
debug!("returns: {:?}", response); debug!("returns: {:?}", response);
@ -42,8 +38,7 @@ struct VersionResponse {
pkg_version: String, pkg_version: String,
} }
#[get("/version", wrap = "Authentication::Private")] async fn get_version(_data: GuardedData<Private, Data>) -> HttpResponse {
async fn get_version() -> HttpResponse {
let commit_sha = match option_env!("COMMIT_SHA") { let commit_sha = match option_env!("COMMIT_SHA") {
Some("") | None => env!("VERGEN_SHA"), Some("") | None => env!("VERGEN_SHA"),
Some(commit_sha) => commit_sha, Some(commit_sha) => commit_sha,

View File

@ -14,8 +14,8 @@ async fn delete_one_unexisting_document() {
let server = Server::new().await; let server = Server::new().await;
let index = server.index("test"); let index = server.index("test");
index.create(None).await; index.create(None).await;
let (_response, code) = index.delete_document(0).await; let (response, code) = index.delete_document(0).await;
assert_eq!(code, 202); assert_eq!(code, 202, "{}", response);
let update = index.wait_update_id(0).await; let update = index.wait_update_id(0).await;
assert_eq!(update["status"], "processed"); assert_eq!(update["status"], "processed");
} }
@ -85,8 +85,8 @@ async fn clear_all_documents_empty_index() {
#[actix_rt::test] #[actix_rt::test]
async fn delete_batch_unexisting_index() { async fn delete_batch_unexisting_index() {
let server = Server::new().await; let server = Server::new().await;
let (_response, code) = server.index("test").delete_batch(vec![]).await; let (response, code) = server.index("test").delete_batch(vec![]).await;
assert_eq!(code, 404); assert_eq!(code, 404, "{}", response);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde_json::{Value, json}; use serde_json::{json, Value};
use crate::common::Server; use crate::common::Server;
@ -11,7 +11,10 @@ static DEFAULT_SETTINGS_VALUES: Lazy<HashMap<&'static str, Value>> = Lazy::new(|
map.insert("searchable_attributes", json!(["*"])); map.insert("searchable_attributes", json!(["*"]));
map.insert("filterable_attributes", json!([])); map.insert("filterable_attributes", json!([]));
map.insert("distinct_attribute", json!(Value::Null)); 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("stop_words", json!([]));
map.insert("synonyms", json!({})); map.insert("synonyms", json!({}));
map map