diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index e74f1707c..4dbf1bf6f 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -188,6 +188,12 @@ impl AuthFilter { self.allow_index_creation && self.is_index_authorized(index) } + #[inline] + /// Return true if a tenant token was used to generate the search rules. + pub fn is_tenant_token(&self) -> bool { + self.search_rules.is_some() + } + pub fn with_allowed_indexes(allowed_indexes: HashSet) -> Self { Self { search_rules: None, @@ -205,6 +211,7 @@ impl AuthFilter { .unwrap_or(true) } + /// Check if the index is authorized by the API key and the tenant token. pub fn is_index_authorized(&self, index: &str) -> bool { self.key_authorized_indexes.is_index_authorized(index) && self @@ -214,6 +221,44 @@ impl AuthFilter { .unwrap_or(true) } + /// Only check if the index is authorized by the API key + pub fn api_key_is_index_authorized(&self, index: &str) -> bool { + self.key_authorized_indexes.is_index_authorized(index) + } + + /// Only check if the index is authorized by the tenant token + pub fn tenant_token_is_index_authorized(&self, index: &str) -> bool { + self.search_rules + .as_ref() + .map(|search_rules| search_rules.is_index_authorized(index)) + .unwrap_or(true) + } + + /// Return the list of authorized indexes by the tenant token if any + pub fn tenant_token_list_index_authorized(&self) -> Vec { + match self.search_rules { + Some(ref search_rules) => { + let mut indexes: Vec<_> = match search_rules { + SearchRules::Set(set) => set.iter().map(|s| s.to_string()).collect(), + SearchRules::Map(map) => map.keys().map(|s| s.to_string()).collect(), + }; + indexes.sort_unstable(); + indexes + } + None => Vec::new(), + } + } + + /// Return the list of authorized indexes by the api key if any + pub fn api_key_list_index_authorized(&self) -> Vec { + let mut indexes: Vec<_> = match self.key_authorized_indexes { + SearchRules::Set(ref set) => set.iter().map(|s| s.to_string()).collect(), + SearchRules::Map(ref map) => map.keys().map(|s| s.to_string()).collect(), + }; + indexes.sort_unstable(); + indexes + } + pub fn get_index_search_rules(&self, index: &str) -> Option { if !self.is_index_authorized(index) { return None; diff --git a/meilisearch/src/extractors/authentication/mod.rs b/meilisearch/src/extractors/authentication/mod.rs index 007e2be40..28a6d770e 100644 --- a/meilisearch/src/extractors/authentication/mod.rs +++ b/meilisearch/src/extractors/authentication/mod.rs @@ -12,6 +12,8 @@ use futures::Future; use meilisearch_auth::{AuthController, AuthFilter}; use meilisearch_types::error::{Code, ResponseError}; +use self::policies::AuthError; + pub struct GuardedData { data: D, filters: AuthFilter, @@ -35,12 +37,12 @@ impl GuardedData { let missing_master_key = auth.get_master_key().is_none(); match Self::authenticate(auth, token, index).await? { - Some(filters) => match data { + Ok(filters) => match data { Some(data) => Ok(Self { data, filters, _marker: PhantomData }), None => Err(AuthenticationError::IrretrievableState.into()), }, - None if missing_master_key => Err(AuthenticationError::MissingMasterKey.into()), - None => Err(AuthenticationError::InvalidToken.into()), + Err(_) if missing_master_key => Err(AuthenticationError::MissingMasterKey.into()), + Err(e) => Err(ResponseError::from_msg(e.to_string(), Code::InvalidApiKey)), } } @@ -51,12 +53,12 @@ impl GuardedData { let missing_master_key = auth.get_master_key().is_none(); match Self::authenticate(auth, String::new(), None).await? { - Some(filters) => match data { + Ok(filters) => match data { Some(data) => Ok(Self { data, filters, _marker: PhantomData }), None => Err(AuthenticationError::IrretrievableState.into()), }, - None if missing_master_key => Err(AuthenticationError::MissingMasterKey.into()), - None => Err(AuthenticationError::MissingAuthorizationHeader.into()), + Err(_) if missing_master_key => Err(AuthenticationError::MissingMasterKey.into()), + Err(_) => Err(AuthenticationError::MissingAuthorizationHeader.into()), } } @@ -64,7 +66,7 @@ impl GuardedData { auth: Data, token: String, index: Option, - ) -> Result, ResponseError> + ) -> Result, ResponseError> where P: Policy + 'static, { @@ -127,13 +129,14 @@ pub trait Policy { auth: Data, token: &str, index: Option<&str>, - ) -> Option; + ) -> Result; } pub mod policies { use actix_web::web::Data; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use meilisearch_auth::{AuthController, AuthFilter, SearchRules}; + use meilisearch_types::error::{Code, ErrorCode}; // reexport actions in policies in order to be used in routes configuration. pub use meilisearch_types::keys::{actions, Action}; use serde::{Deserialize, Serialize}; @@ -144,11 +147,53 @@ pub mod policies { enum TenantTokenOutcome { NotATenantToken, - Invalid, - Expired, Valid(Uuid, SearchRules), } + #[derive(thiserror::Error, Debug)] + pub enum AuthError { + #[error("Tenant token expired. Was valid up to `{exp}` and we're now `{now}`.")] + ExpiredTenantToken { exp: i64, now: i64 }, + #[error("The provided API key is invalid.")] + InvalidApiKey, + #[error("The provided tenant token cannot acces the index `{index}`, allowed indexes are {allowed:?}.")] + TenantTokenAccessingnUnauthorizedIndex { index: String, allowed: Vec }, + #[error( + "The API key used to generate this tenant token cannot acces the index `{index}`." + )] + TenantTokenApiKeyAccessingnUnauthorizedIndex { index: String }, + #[error( + "The API key cannot acces the index `{index}`, authorized indexes are {allowed:?}." + )] + ApiKeyAccessingnUnauthorizedIndex { index: String, allowed: Vec }, + #[error("The provided tenant token is invalid.")] + InvalidTenantToken, + #[error("Could not decode tenant token, {0}.")] + CouldNotDecodeTenantToken(jsonwebtoken::errors::Error), + #[error("Invalid action `{0}`.")] + InternalInvalidAction(u8), + } + + impl From for AuthError { + fn from(error: jsonwebtoken::errors::Error) -> Self { + use jsonwebtoken::errors::ErrorKind; + + match error.kind() { + ErrorKind::InvalidToken => AuthError::InvalidTenantToken, + _ => AuthError::CouldNotDecodeTenantToken(error), + } + } + } + + impl ErrorCode for AuthError { + fn error_code(&self) -> Code { + match self { + AuthError::InternalInvalidAction(_) => Code::Internal, + _ => Code::InvalidApiKey, + } + } + } + fn tenant_token_validation() -> Validation { let mut validation = Validation::default(); validation.validate_exp = false; @@ -158,15 +203,15 @@ pub mod policies { } /// Extracts the key id used to sign the payload, without performing any validation. - fn extract_key_id(token: &str) -> Option { + fn extract_key_id(token: &str) -> Result { let mut validation = tenant_token_validation(); validation.insecure_disable_signature_validation(); let dummy_key = DecodingKey::from_secret(b"secret"); - let token_data = decode::(token, &dummy_key, &validation).ok()?; + let token_data = decode::(token, &dummy_key, &validation)?; // get token fields without validating it. let Claims { api_key_uid, .. } = token_data.claims; - Some(api_key_uid) + Ok(api_key_uid) } fn is_keys_action(action: u8) -> bool { @@ -187,76 +232,102 @@ pub mod policies { auth: Data, token: &str, index: Option<&str>, - ) -> Option { + ) -> Result { // authenticate if token is the master key. // Without a master key, all routes are accessible except the key-related routes. if auth.get_master_key().map_or_else(|| !is_keys_action(A), |mk| mk == token) { - return Some(AuthFilter::default()); + return Ok(AuthFilter::default()); } let (key_uuid, search_rules) = match ActionPolicy::::authenticate_tenant_token(&auth, token) { - TenantTokenOutcome::Valid(key_uuid, search_rules) => { + Ok(TenantTokenOutcome::Valid(key_uuid, search_rules)) => { (key_uuid, Some(search_rules)) } - TenantTokenOutcome::Expired => return None, - TenantTokenOutcome::Invalid => return None, - TenantTokenOutcome::NotATenantToken => { - (auth.get_optional_uid_from_encoded_key(token.as_bytes()).ok()??, None) - } + Ok(TenantTokenOutcome::NotATenantToken) + | Err(AuthError::InvalidTenantToken) => ( + auth.get_optional_uid_from_encoded_key(token.as_bytes()) + .map_err(|_e| AuthError::InvalidApiKey)? + .ok_or(AuthError::InvalidApiKey)?, + None, + ), + Err(e) => return Err(e), }; // check that the indexes are allowed - let action = Action::from_repr(A)?; - let auth_filter = auth.get_key_filters(key_uuid, search_rules).ok()?; - if auth.is_key_authorized(key_uuid, action, index).unwrap_or(false) - && index.map(|index| auth_filter.is_index_authorized(index)).unwrap_or(true) - { - return Some(auth_filter); + let action = Action::from_repr(A).ok_or(AuthError::InternalInvalidAction(A))?; + let auth_filter = auth + .get_key_filters(key_uuid, search_rules) + .map_err(|_e| AuthError::InvalidApiKey)?; + + // First check if the index is authorized in the tenant token, this is a public + // information, we can return a nice error message. + if let Some(index) = index { + if !auth_filter.tenant_token_is_index_authorized(index) { + return Err(AuthError::TenantTokenAccessingnUnauthorizedIndex { + index: index.to_string(), + allowed: auth_filter.tenant_token_list_index_authorized(), + }); + } + if !auth_filter.api_key_is_index_authorized(index) { + if auth_filter.is_tenant_token() { + // If the error comes from a tenant token we cannot share the list + // of authorized indexes in the API key. This is not public information. + return Err(AuthError::TenantTokenApiKeyAccessingnUnauthorizedIndex { + index: index.to_string(), + }); + } else { + // Otherwise we can share the list + // of authorized indexes in the API key. + return Err(AuthError::ApiKeyAccessingnUnauthorizedIndex { + index: index.to_string(), + allowed: auth_filter.api_key_list_index_authorized(), + }); + } + } + } + if auth.is_key_authorized(key_uuid, action, index).unwrap_or(false) { + return Ok(auth_filter); } - None + Err(AuthError::InvalidApiKey) } } impl ActionPolicy { - fn authenticate_tenant_token(auth: &AuthController, token: &str) -> TenantTokenOutcome { + fn authenticate_tenant_token( + auth: &AuthController, + token: &str, + ) -> Result { // Only search action can be accessed by a tenant token. if A != actions::SEARCH { - return TenantTokenOutcome::NotATenantToken; + return Ok(TenantTokenOutcome::NotATenantToken); } - let uid = if let Some(uid) = extract_key_id(token) { - uid - } else { - return TenantTokenOutcome::NotATenantToken; - }; + let uid = extract_key_id(token)?; // Check if tenant token is valid. let key = if let Some(key) = auth.generate_key(uid) { key } else { - return TenantTokenOutcome::Invalid; + return Err(AuthError::InvalidTenantToken); }; - let data = if let Ok(data) = decode::( + let data = decode::( token, &DecodingKey::from_secret(key.as_bytes()), &tenant_token_validation(), - ) { - data - } else { - return TenantTokenOutcome::Invalid; - }; + )?; // Check if token is expired. if let Some(exp) = data.claims.exp { - if OffsetDateTime::now_utc().unix_timestamp() > exp { - return TenantTokenOutcome::Expired; + let now = OffsetDateTime::now_utc().unix_timestamp(); + if now > exp { + return Err(AuthError::ExpiredTenantToken { exp, now }); } } - TenantTokenOutcome::Valid(uid, data.claims.search_rules) + Ok(TenantTokenOutcome::Valid(uid, data.claims.search_rules)) } } diff --git a/meilisearch/tests/auth/authorization.rs b/meilisearch/tests/auth/authorization.rs index d26bb26b8..609b7d01b 100644 --- a/meilisearch/tests/auth/authorization.rs +++ b/meilisearch/tests/auth/authorization.rs @@ -78,7 +78,7 @@ pub static ALL_ACTIONS: Lazy> = Lazy::new(|| { }); static INVALID_RESPONSE: Lazy = Lazy::new(|| { - json!({"message": "The provided API key is invalid.", + json!({"message": null, "code": "invalid_api_key", "type": "auth", "link": "https://docs.meilisearch.com/errors#invalid_api_key" @@ -119,7 +119,8 @@ async fn error_access_expired_key() { thread::sleep(time::Duration::new(1, 0)); for (method, route) in AUTHORIZATIONS.keys() { - let (response, code) = server.dummy_request(method, route).await; + let (mut response, code) = server.dummy_request(method, route).await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone(), "on route: {:?} - {:?}", method, route); assert_eq!(403, code, "{:?}", &response); @@ -149,7 +150,8 @@ async fn error_access_unauthorized_index() { // filter `products` index routes .filter(|(_, route)| route.starts_with("/indexes/products")) { - let (response, code) = server.dummy_request(method, route).await; + let (mut response, code) = server.dummy_request(method, route).await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone(), "on route: {:?} - {:?}", method, route); assert_eq!(403, code, "{:?}", &response); @@ -176,7 +178,8 @@ async fn error_access_unauthorized_action() { let key = response["key"].as_str().unwrap(); server.use_api_key(key); - let (response, code) = server.dummy_request(method, route).await; + let (mut response, code) = server.dummy_request(method, route).await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone(), "on route: {:?} - {:?}", method, route); assert_eq!(403, code, "{:?}", &response); @@ -280,7 +283,7 @@ async fn access_authorized_no_index_restriction() { route, action ); - assert_ne!(code, 403); + assert_ne!(code, 403, "on route: {:?} - {:?} with action: {:?}", method, route, action); } } } diff --git a/meilisearch/tests/auth/errors.rs b/meilisearch/tests/auth/errors.rs index 581243a0a..466eefe65 100644 --- a/meilisearch/tests/auth/errors.rs +++ b/meilisearch/tests/auth/errors.rs @@ -1,7 +1,10 @@ +use actix_web::test; +use http::StatusCode; +use jsonwebtoken::{EncodingKey, Header}; use meili_snap::*; use uuid::Uuid; -use crate::common::Server; +use crate::common::{Server, Value}; use crate::json; #[actix_rt::test] @@ -436,3 +439,262 @@ async fn patch_api_keys_unknown_field() { } "###); } + +async fn send_request_with_custom_auth( + app: impl actix_web::dev::Service< + actix_http::Request, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + url: &str, + auth: &str, +) -> (Value, StatusCode) { + let req = test::TestRequest::get().uri(url).insert_header(("Authorization", auth)).to_request(); + let res = test::call_service(&app, req).await; + let status_code = res.status(); + let body = test::read_body(res).await; + let response: Value = serde_json::from_slice(&body).unwrap_or_default(); + + (response, status_code) +} + +#[actix_rt::test] +async fn invalid_auth_format() { + let server = Server::new_auth().await; + let app = server.init_web_app().await; + + let req = test::TestRequest::get().uri("/indexes/dog/documents").to_request(); + let res = test::call_service(&app, req).await; + let status_code = res.status(); + let body = test::read_body(res).await; + let response: Value = serde_json::from_slice(&body).unwrap_or_default(); + snapshot!(status_code, @"401 Unauthorized"); + snapshot!(response, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + "###); + + let req = test::TestRequest::get().uri("/indexes/dog/documents").to_request(); + let res = test::call_service(&app, req).await; + let status_code = res.status(); + let body = test::read_body(res).await; + let response: Value = serde_json::from_slice(&body).unwrap_or_default(); + snapshot!(status_code, @"401 Unauthorized"); + snapshot!(response, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + "###); + + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/documents", "Bearer").await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); +} + +#[actix_rt::test] +async fn invalid_api_key() { + let server = Server::new_auth().await; + let app = server.init_web_app().await; + + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/search", "Bearer kefir").await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + let uuid = Uuid::nil(); + let key = json!({ "actions": ["search"], "indexes": ["dog"], "expiresAt": null, "uid": uuid.to_string() }); + let req = test::TestRequest::post() + .uri("/keys") + .insert_header(("Authorization", "Bearer MASTER_KEY")) + .set_json(&key) + .to_request(); + let res = test::call_service(&app, req).await; + let body = test::read_body(res).await; + let response: Value = serde_json::from_slice(&body).unwrap_or_default(); + snapshot!(json_string!(response, { ".createdAt" => "[date]", ".updatedAt" => "[date]" }), @r###" + { + "name": null, + "description": null, + "key": "aeb94973e0b6e912d94165430bbe87dee91a7c4f891ce19050c3910ec96977e9", + "uid": "00000000-0000-0000-0000-000000000000", + "actions": [ + "search" + ], + "indexes": [ + "dog" + ], + "expiresAt": null, + "createdAt": "[date]", + "updatedAt": "[date]" + } + "###); + let key = response["key"].as_str().unwrap(); + + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/doggo/search", &format!("Bearer {key}")) + .await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The API key cannot acces the index `doggo`, authorized indexes are [\"dog\"].", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); +} + +#[actix_rt::test] +async fn invalid_tenant_token() { + let server = Server::new_auth().await; + let app = server.init_web_app().await; + + // The tenant token won't be recognized at all if we're not on a search route + let claims = json!({ "tamo": "kefir" }); + let jwt = jsonwebtoken::encode(&Header::default(), &claims, &EncodingKey::from_secret(b"tamo")) + .unwrap(); + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/documents", &format!("Bearer {jwt}")) + .await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + let claims = json!({ "tamo": "kefir" }); + let jwt = jsonwebtoken::encode(&Header::default(), &claims, &EncodingKey::from_secret(b"tamo")) + .unwrap(); + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/search", &format!("Bearer {jwt}")).await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "Could not decode tenant token, JSON error: missing field `searchRules` at line 1 column 16.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + // The error messages are not ideal but that's expected since we cannot _yet_ use deserr + let claims = json!({ "searchRules": "kefir" }); + let jwt = jsonwebtoken::encode(&Header::default(), &claims, &EncodingKey::from_secret(b"tamo")) + .unwrap(); + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/search", &format!("Bearer {jwt}")).await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "Could not decode tenant token, JSON error: data did not match any variant of untagged enum SearchRules at line 1 column 23.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + let uuid = Uuid::nil(); + let claims = json!({ "searchRules": ["kefir"], "apiKeyUid": uuid.to_string() }); + let jwt = jsonwebtoken::encode(&Header::default(), &claims, &EncodingKey::from_secret(b"tamo")) + .unwrap(); + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/search", &format!("Bearer {jwt}")).await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "Could not decode tenant token, InvalidSignature.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + // ~~ For the next tests we first need a valid API key + let key = json!({ "actions": ["search"], "indexes": ["dog"], "expiresAt": null, "uid": uuid.to_string() }); + let req = test::TestRequest::post() + .uri("/keys") + .insert_header(("Authorization", "Bearer MASTER_KEY")) + .set_json(&key) + .to_request(); + let res = test::call_service(&app, req).await; + let body = test::read_body(res).await; + let response: Value = serde_json::from_slice(&body).unwrap_or_default(); + snapshot!(json_string!(response, { ".createdAt" => "[date]", ".updatedAt" => "[date]" }), @r###" + { + "name": null, + "description": null, + "key": "aeb94973e0b6e912d94165430bbe87dee91a7c4f891ce19050c3910ec96977e9", + "uid": "00000000-0000-0000-0000-000000000000", + "actions": [ + "search" + ], + "indexes": [ + "dog" + ], + "expiresAt": null, + "createdAt": "[date]", + "updatedAt": "[date]" + } + "###); + let key = response["key"].as_str().unwrap(); + + let claims = json!({ "searchRules": ["doggo", "catto"], "apiKeyUid": uuid.to_string() }); + let jwt = jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(key.as_bytes()), + ) + .unwrap(); + // Try to access an index that is not authorized by the tenant token + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/dog/search", &format!("Bearer {jwt}")).await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The provided tenant token cannot acces the index `dog`, allowed indexes are [\"catto\", \"doggo\"].", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); + + // Try to access an index that *is* authorized by the tenant token but not by the api key used to generate the tt + let (response, status_code) = + send_request_with_custom_auth(&app, "/indexes/doggo/search", &format!("Bearer {jwt}")) + .await; + snapshot!(status_code, @"403 Forbidden"); + snapshot!(response, @r###" + { + "message": "The API key used to generate this tenant token cannot acces the index `doggo`.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid_api_key" + } + "###); +} diff --git a/meilisearch/tests/auth/tenant_token.rs b/meilisearch/tests/auth/tenant_token.rs index ba3b0b234..5e8a75c36 100644 --- a/meilisearch/tests/auth/tenant_token.rs +++ b/meilisearch/tests/auth/tenant_token.rs @@ -53,7 +53,8 @@ static DOCUMENTS: Lazy = Lazy::new(|| { }); static INVALID_RESPONSE: Lazy = Lazy::new(|| { - json!({"message": "The provided API key is invalid.", + json!({ + "message": null, "code": "invalid_api_key", "type": "auth", "link": "https://docs.meilisearch.com/errors#invalid_api_key" @@ -191,7 +192,9 @@ macro_rules! compute_forbidden_search { server.use_api_key(&web_token); let index = server.index("sales"); index - .search(json!({}), |response, code| { + .search(json!({}), |mut response, code| { + // We don't assert anything on the message since it may change between cases + response["message"] = serde_json::json!(null); assert_eq!( response, INVALID_RESPONSE.clone(), @@ -495,7 +498,8 @@ async fn error_access_forbidden_routes() { for ((method, route), actions) in AUTHORIZATIONS.iter() { if !actions.contains("search") { - let (response, code) = server.dummy_request(method, route).await; + let (mut response, code) = server.dummy_request(method, route).await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(code, 403); } @@ -529,14 +533,16 @@ async fn error_access_expired_parent_key() { server.use_api_key(&web_token); // test search request while parent_key is not expired - let (response, code) = server.dummy_request("POST", "/indexes/products/search").await; + let (mut response, code) = server.dummy_request("POST", "/indexes/products/search").await; + response["message"] = serde_json::json!(null); assert_ne!(response, INVALID_RESPONSE.clone()); assert_ne!(code, 403); // wait until the key is expired. thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server.dummy_request("POST", "/indexes/products/search").await; + let (mut response, code) = server.dummy_request("POST", "/indexes/products/search").await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(code, 403); } @@ -585,7 +591,8 @@ async fn error_access_modified_token() { .join("."); server.use_api_key(&altered_token); - let (response, code) = server.dummy_request("POST", "/indexes/products/search").await; + let (mut response, code) = server.dummy_request("POST", "/indexes/products/search").await; + response["message"] = serde_json::json!(null); assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(code, 403); } diff --git a/meilisearch/tests/auth/tenant_token_multi_search.rs b/meilisearch/tests/auth/tenant_token_multi_search.rs index 09b5dbbcc..81146d14e 100644 --- a/meilisearch/tests/auth/tenant_token_multi_search.rs +++ b/meilisearch/tests/auth/tenant_token_multi_search.rs @@ -109,9 +109,11 @@ static NESTED_DOCUMENTS: Lazy = Lazy::new(|| { fn invalid_response(query_index: Option) -> Value { let message = if let Some(query_index) = query_index { - format!("Inside `.queries[{query_index}]`: The provided API key is invalid.") + json!(format!("Inside `.queries[{query_index}]`: The provided API key is invalid.")) } else { - "The provided API key is invalid.".to_string() + // if it's anything else we simply return null and will tests all the + // error messages somewhere else + json!(null) }; json!({"message": message, "code": "invalid_api_key", @@ -414,7 +416,10 @@ macro_rules! compute_forbidden_single_search { for (tenant_token, failed_query_index) in $tenant_tokens.iter().zip(failed_query_indexes.into_iter()) { let web_token = generate_tenant_token(&uid, &key, tenant_token.clone()); server.use_api_key(&web_token); - let (response, code) = server.multi_search(json!({"queries" : [{"indexUid": "sales"}]})).await; + let (mut response, code) = server.multi_search(json!({"queries" : [{"indexUid": "sales"}]})).await; + if failed_query_index.is_none() && !response["message"].is_null() { + response["message"] = serde_json::json!(null); + } assert_eq!( response, invalid_response(failed_query_index), @@ -469,10 +474,13 @@ macro_rules! compute_forbidden_multiple_search { for (tenant_token, failed_query_index) in $tenant_tokens.iter().zip(failed_query_indexes.into_iter()) { let web_token = generate_tenant_token(&uid, &key, tenant_token.clone()); server.use_api_key(&web_token); - let (response, code) = server.multi_search(json!({"queries" : [ + let (mut response, code) = server.multi_search(json!({"queries" : [ {"indexUid": "sales"}, {"indexUid": "products"}, ]})).await; + if failed_query_index.is_none() && !response["message"].is_null() { + response["message"] = serde_json::json!(null); + } assert_eq!( response, invalid_response(failed_query_index), @@ -1073,18 +1081,20 @@ async fn error_access_expired_parent_key() { server.use_api_key(&web_token); // test search request while parent_key is not expired - let (response, code) = server + let (mut response, code) = server .multi_search(json!({"queries" : [{"indexUid": "sales"}, {"indexUid": "products"}]})) .await; + response["message"] = serde_json::json!(null); assert_ne!(response, invalid_response(None)); assert_ne!(code, 403); // wait until the key is expired. thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server + let (mut response, code) = server .multi_search(json!({"queries" : [{"indexUid": "sales"}, {"indexUid": "products"}]})) .await; + response["message"] = serde_json::json!(null); assert_eq!(response, invalid_response(None)); assert_eq!(code, 403); } @@ -1134,8 +1144,9 @@ async fn error_access_modified_token() { .join("."); server.use_api_key(&altered_token); - let (response, code) = + let (mut response, code) = server.multi_search(json!({"queries" : [{"indexUid": "products"}]})).await; + response["message"] = serde_json::json!(null); assert_eq!(response, invalid_response(None)); assert_eq!(code, 403); } diff --git a/meilisearch/tests/common/mod.rs b/meilisearch/tests/common/mod.rs index 3117dd185..1391cf7cf 100644 --- a/meilisearch/tests/common/mod.rs +++ b/meilisearch/tests/common/mod.rs @@ -42,6 +42,12 @@ impl std::ops::Deref for Value { } } +impl std::ops::DerefMut for Value { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl PartialEq for Value { fn eq(&self, other: &serde_json::Value) -> bool { &self.0 == other