From 7495233025209ef5aa2b2d20bf568a228d0be9c8 Mon Sep 17 00:00:00 2001 From: Mubelotix Date: Thu, 17 Jul 2025 10:20:20 +0200 Subject: [PATCH] Update authorization tests --- .../meilisearch/tests/auth/authorization.rs | 109 +++++++++++++++--- 1 file changed, 90 insertions(+), 19 deletions(-) diff --git a/crates/meilisearch/tests/auth/authorization.rs b/crates/meilisearch/tests/auth/authorization.rs index 277911fb8..b7794908b 100644 --- a/crates/meilisearch/tests/auth/authorization.rs +++ b/crates/meilisearch/tests/auth/authorization.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use ::time::format_description::well_known::Rfc3339; -use maplit::{hashmap, hashset}; +use maplit::{hashmap}; use meilisearch::Opt; use once_cell::sync::Lazy; use tempfile::TempDir; @@ -10,7 +10,30 @@ use time::{Duration, OffsetDateTime}; use crate::common::{default_settings, Server, Value}; use crate::json; -pub static AUTHORIZATIONS: Lazy>> = +macro_rules! hashset { + ( $( $val:tt ),* $(,)? ) => {{ + let mut set: HashSet<&'static [&'static str]> = HashSet::new(); + $( + hashset!(@insert set, $val); + )* + set + }}; + + // Match array-like input: ["a", "b"] + (@insert $set:ident, [ $($elem:literal),* ]) => {{ + const ITEM: &[&str] = &[$($elem),*]; + $set.insert(ITEM); + }}; + + // Match single literal: "a" + (@insert $set:ident, $val:literal) => {{ + const ITEM: &[&str] = &[$val]; + $set.insert(ITEM); + }}; +} + +#[allow(clippy::type_complexity)] +pub static AUTHORIZATIONS: Lazy>> = Lazy::new(|| { let authorizations = hashmap! { ("POST", "/multi-search") => hashset!{"search", "*"}, @@ -23,6 +46,7 @@ pub static AUTHORIZATIONS: Lazy hashset!{"documents.delete", "documents.*", "*"}, ("POST", "/indexes/products/documents/delete-batch") => hashset!{"documents.delete", "documents.*", "*"}, ("POST", "/indexes/products/documents/delete") => hashset!{"documents.delete", "documents.*", "*"}, + ("POST", "/indexes/products/render") => hashset!{["settings.get", "documents.get"], ["documents.*", "settings.get"], ["settings.*", "documents.get"], "*"}, ("GET", "/tasks") => hashset!{"tasks.get", "tasks.*", "*"}, ("DELETE", "/tasks") => hashset!{"tasks.delete", "tasks.*", "*"}, ("GET", "/tasks?indexUid=products") => hashset!{"tasks.get", "tasks.*", "*"}, @@ -68,7 +92,7 @@ pub static AUTHORIZATIONS: Lazy hashset!{"keys.get", "*"}, ("GET", "/experimental-features") => hashset!{"experimental.get", "*"}, ("PATCH", "/experimental-features") => hashset!{"experimental.update", "*"}, - ("GET", "/network") => hashset!{"network.get", "*"}, + ("GET", "/network") => hashset!{"network.get", "*"}, ("PATCH", "/network") => hashset!{"network.update", "*"}, }; @@ -76,7 +100,7 @@ pub static AUTHORIZATIONS: Lazy> = Lazy::new(|| { - AUTHORIZATIONS.values().cloned().reduce(|l, r| l.union(&r).cloned().collect()).unwrap() + AUTHORIZATIONS.values().flat_map(|v| v.iter()).flat_map(|v| v.iter()).copied().collect::>() }); static INVALID_RESPONSE: Lazy = Lazy::new(|| { @@ -164,13 +188,14 @@ async fn error_access_unauthorized_index() { async fn error_access_unauthorized_action() { let mut server = Server::new_auth().await; - for ((method, route), action) in AUTHORIZATIONS.iter() { + for ((method, route), actions) in AUTHORIZATIONS.iter() { // create a new API key letting only the needed action. server.use_api_key(MASTER_KEY); + let actions = actions.iter().flat_map(|s| s.iter()).copied().collect::>(); let content = json!({ "indexes": ["products"], - "actions": ALL_ACTIONS.difference(action).collect::>(), + "actions": ALL_ACTIONS.difference(&actions).collect::>(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); @@ -194,7 +219,7 @@ async fn access_authorized_master_key() { server.use_api_key(MASTER_KEY); // master key must have access to all routes. - for ((method, route), _) in AUTHORIZATIONS.iter() { + for (method, route) in AUTHORIZATIONS.keys() { let (response, code) = server.dummy_request(method, route).await; assert_ne!(response, INVALID_RESPONSE.clone(), "on route: {:?} - {:?}", method, route); @@ -208,13 +233,13 @@ async fn access_authorized_restricted_index() { let enable_metrics = Opt { experimental_enable_metrics: true, ..default_settings(dir.path()) }; let mut server = Server::new_auth_with_options(enable_metrics, dir).await; for ((method, route), actions) in AUTHORIZATIONS.iter() { - for action in actions { + for actions in actions { // create a new API key letting only the needed action. server.use_api_key(MASTER_KEY); let content = json!({ "indexes": ["products"], - "actions": [action], + "actions": actions, "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); @@ -232,20 +257,20 @@ async fn access_authorized_restricted_index() { assert_eq!( response, INVALID_METRICS_RESPONSE.clone(), - "on route: {:?} - {:?} with action: {:?}", + "on route: {:?} - {:?} with actions: {:?}", method, route, - action + actions ); assert_eq!(code, 403); } else { assert_ne!( response, INVALID_RESPONSE.clone(), - "on route: {:?} - {:?} with action: {:?}", + "on route: {:?} - {:?} with actions: {:?}", method, route, - action + actions ); assert_ne!(code, 403); } @@ -253,18 +278,63 @@ async fn access_authorized_restricted_index() { } } +#[actix_rt::test] +async fn unauthorized_partial_actions() { + let mut server = Server::new_auth().await; + server.use_admin_key(MASTER_KEY).await; + + // create index `products` + let index = server.index("products"); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); + let task_id = response["taskUid"].as_u64().unwrap(); + index.wait_task(task_id).await.succeeded(); + + // When multiple actions are necessary, the server mustn't accept any combination with one action missing. + for ((method, route), actions) in AUTHORIZATIONS.iter() { + for actions in actions { + if 2 <= actions.len() { + for excluded_action in *actions { + // create a new API key letting all actions except one. + server.use_api_key(MASTER_KEY); + + let actions = actions.iter().filter(|&a| a != excluded_action).copied().collect::>(); + let content = json!({ + "indexes": ["products"], + "actions": actions, + "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), + }); + + let (response, code) = server.add_api_key(content).await; + assert_eq!(201, code, "{:?}", &response); + assert!(response["key"].is_string()); + + let key = response["key"].as_str().unwrap(); + server.use_api_key(key); + + let (mut response, code) = server.dummy_request(method, route).await; + response["message"] = serde_json::json!(null); + + assert_eq!(response, INVALID_RESPONSE.clone(), "on route: {:?} - {:?} with actions: {:?}", method, route, actions); + assert_eq!(code, 403, "{:?}", &response); + } + } + } + } +} + #[actix_rt::test] async fn access_authorized_no_index_restriction() { let mut server = Server::new_auth().await; for ((method, route), actions) in AUTHORIZATIONS.iter() { - for action in actions { + for actions in actions { // create a new API key letting only the needed action. server.use_api_key(MASTER_KEY); let content = json!({ "indexes": ["*"], - "actions": [action], + "actions": actions, "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); @@ -280,12 +350,12 @@ async fn access_authorized_no_index_restriction() { assert_ne!( response, INVALID_RESPONSE.clone(), - "on route: {:?} - {:?} with action: {:?}", + "on route: {:?} - {:?} with actions: {:?}", method, route, - action + actions ); - assert_ne!(code, 403, "on route: {:?} - {:?} with action: {:?}", method, route, action); + assert_ne!(code, 403, "on route: {:?} - {:?} with action: {:?}", method, route, actions); } } } @@ -723,10 +793,11 @@ async fn error_creating_index_without_action() { server.use_api_key(MASTER_KEY); // create key with access on all indexes. + let create_index_actions = AUTHORIZATIONS.get(&("POST","/indexes")).unwrap().iter().flat_map(|s| s.iter()).cloned().collect::>(); let content = json!({ "indexes": ["*"], // Give all action but the ones allowing to create an index. - "actions": ALL_ACTIONS.iter().cloned().filter(|a| !AUTHORIZATIONS.get(&("POST","/indexes")).unwrap().contains(a)).collect::>(), + "actions": ALL_ACTIONS.iter().cloned().filter(|a| !create_index_actions.contains(a)).collect::>(), "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await;