use std::collections::HashMap; use ::time::format_description::well_known::Rfc3339; use maplit::hashmap; use once_cell::sync::Lazy; use serde_json::{json, Value}; use time::{Duration, OffsetDateTime}; use super::authorization::ALL_ACTIONS; use crate::common::Server; fn generate_tenant_token( parent_uid: impl AsRef, parent_key: impl AsRef, mut body: HashMap<&str, Value>, ) -> String { use jsonwebtoken::{encode, EncodingKey, Header}; let parent_uid = parent_uid.as_ref(); body.insert("apiKeyUid", json!(parent_uid)); encode(&Header::default(), &body, &EncodingKey::from_secret(parent_key.as_ref().as_bytes())) .unwrap() } static DOCUMENTS: Lazy = Lazy::new(|| { json!([ { "title": "Shazam!", "id": "287947", "color": ["green", "blue"] }, { "title": "Captain Marvel", "id": "299537", "color": ["yellow", "blue"] }, { "title": "Escape Room", "id": "522681", "color": ["yellow", "red"] }, { "title": "How to Train Your Dragon: The Hidden World", "id": "166428", "color": ["green", "red"] }, { "title": "Glass", "id": "450465", "color": ["blue", "red"] } ]) }); static NESTED_DOCUMENTS: Lazy = Lazy::new(|| { json!([ { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2, }, { "name": "buddy", "age": 4, }, ], "cattos": "pesti", }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8, }, ], "cattos": ["simba", "pestiféré"], }, { "id": 750, "father": "romain", "mother": "michelle", "cattos": ["enigma"], }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5, }, { "name": "fast", "age": 6, }, ], "cattos": ["moumoute", "gomez"], }, ]) }); 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.") } else { "The provided API key is invalid.".to_string() }; json!({"message": message, "code": "invalid_api_key", "type": "auth", "link": "https://docs.meilisearch.com/errors#invalid_api_key" }) } static ACCEPTED_KEYS_SINGLE: Lazy> = Lazy::new(|| { vec![ json!({ "indexes": ["*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["*"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sal*", "prod*"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), ] }); static ACCEPTED_KEYS_BOTH: Lazy> = Lazy::new(|| { vec![ json!({ "indexes": ["*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["*"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales", "products"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales", "products"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sal*", "prod*"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), ] }); static SINGLE_REFUSED_KEYS: Lazy> = Lazy::new(|| { vec![ // no search action json!({ "indexes": ["*"], "actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "search" && *a != "*").collect::>(), "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales"], "actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "search" && *a != "*").collect::>(), "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), // bad index json!({ "indexes": ["products"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["prod*", "p*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["products"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), ] }); static BOTH_REFUSED_KEYS: Lazy> = Lazy::new(|| { vec![ // no search action json!({ "indexes": ["*"], "actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "search" && *a != "*").collect::>(), "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales", "products"], "actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "search" && *a != "*").collect::>(), "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), // bad index json!({ "indexes": ["sales"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sales"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["sal*", "proa*"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["products"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["prod*", "p*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), json!({ "indexes": ["products"], "actions": ["search"], "expiresAt": (OffsetDateTime::now_utc() + Duration::days(1)).format(&Rfc3339).unwrap() }), ] }); macro_rules! compute_authorized_single_search { ($tenant_tokens:expr, $filter:expr, $expected_count:expr) => { let mut server = Server::new_auth().await; server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; index .update_settings(json!({"filterableAttributes": ["color"]})) .await; index.wait_task(1).await; drop(index); let index = server.index("products"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; index .update_settings(json!({"filterableAttributes": ["doggos"]})) .await; index.wait_task(3).await; drop(index); for key_content in ACCEPTED_KEYS_SINGLE.iter().chain(ACCEPTED_KEYS_BOTH.iter()) { server.use_api_key("MASTER_KEY"); let (response, code) = server.add_api_key(key_content.clone()).await; assert_eq!(code, 201); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); for tenant_token in $tenant_tokens.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", "filter": $filter}]})).await; assert_eq!( 200, code, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); assert_eq!( $expected_count, response["results"][0]["hits"].as_array().unwrap().len(), "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); } } }; } macro_rules! compute_authorized_multiple_search { ($tenant_tokens:expr, $filter1:expr, $filter2:expr, $expected_count1:expr, $expected_count2:expr) => { let mut server = Server::new_auth().await; server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; index .update_settings(json!({"filterableAttributes": ["color"]})) .await; index.wait_task(1).await; drop(index); let index = server.index("products"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; index .update_settings(json!({"filterableAttributes": ["doggos"]})) .await; index.wait_task(3).await; drop(index); for key_content in ACCEPTED_KEYS_BOTH.iter() { server.use_api_key("MASTER_KEY"); let (response, code) = server.add_api_key(key_content.clone()).await; assert_eq!(code, 201); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); for tenant_token in $tenant_tokens.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", "filter": $filter1}, {"indexUid": "products", "filter": $filter2}, ]})).await; assert_eq!( code, 200, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); assert_eq!( response["results"][0]["hits"].as_array().unwrap().len(), $expected_count1, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); assert_eq!( response["results"][1]["hits"].as_array().unwrap().len(), $expected_count2, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); } } }; } macro_rules! compute_forbidden_single_search { ($tenant_tokens:expr, $parent_keys:expr, $failed_query_indexes:expr) => { let mut server = Server::new_auth().await; server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; index .update_settings(json!({"filterableAttributes": ["color"]})) .await; index.wait_task(1).await; drop(index); let index = server.index("products"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; index .update_settings(json!({"filterableAttributes": ["doggos"]})) .await; index.wait_task(3).await; drop(index); assert_eq!($parent_keys.len(), $failed_query_indexes.len(), "keys != query_indexes"); for (key_content, failed_query_indexes) in $parent_keys.iter().zip($failed_query_indexes.into_iter()) { server.use_api_key("MASTER_KEY"); let (response, code) = server.add_api_key(key_content.clone()).await; assert_eq!(code, 201, "{:?}", response); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); assert_eq!($tenant_tokens.len(), failed_query_indexes.len(), "tenant_tokens != query_indexes"); 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; assert_eq!( response, invalid_response(failed_query_index), "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); assert_eq!( code, 403, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); } } }; } macro_rules! compute_forbidden_multiple_search { ($tenant_tokens:expr, $parent_keys:expr, $failed_query_indexes:expr) => { let mut server = Server::new_auth().await; server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; index .update_settings(json!({"filterableAttributes": ["color"]})) .await; index.wait_task(1).await; drop(index); let index = server.index("products"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; index .update_settings(json!({"filterableAttributes": ["doggos"]})) .await; index.wait_task(3).await; drop(index); assert_eq!($parent_keys.len(), $failed_query_indexes.len(), "keys != query_indexes"); for (key_content, failed_query_indexes) in $parent_keys.iter().zip($failed_query_indexes.into_iter()) { server.use_api_key("MASTER_KEY"); let (response, code) = server.add_api_key(key_content.clone()).await; assert_eq!(code, 201, "{:?}", response); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); assert_eq!($tenant_tokens.len(), failed_query_indexes.len(), "tenant_token != query_indexes"); 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"}, {"indexUid": "products"}, ]})).await; assert_eq!( response, invalid_response(failed_query_index), "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); assert_eq!( code, 403, "{} using tenant_token: {:?} generated with parent_key: {:?}", response, tenant_token, key_content ); } } }; } #[actix_rt::test] async fn single_search_authorized_simple_token() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["*"]), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["sales"]), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["sa*"]), "exp" => Value::Null }, ]; compute_authorized_single_search!(tenant_tokens, {}, 5); } #[actix_rt::test] async fn multi_search_authorized_simple_token() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}, "products": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales", "products"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["*"]), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": {}, "products": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": Value::Null, "products": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["sales", "products"]), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["sa*", "pro*"]), "exp" => Value::Null }, ]; compute_authorized_multiple_search!(tenant_tokens, {}, {}, 5, 4); } #[actix_rt::test] async fn single_search_authorized_filter_token() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {"filter": "color = blue"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": "color = blue"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": {"filter": ["color = blue"]}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": ["color = blue"]}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, // filter on sales should override filters on * hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": "color = blue"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": "color = blue"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": ["color = blue"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": ["color = blue"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_authorized_single_search!(tenant_tokens, {}, 3); } #[actix_rt::test] async fn multi_search_authorized_filter_token() { let both_tenant_tokens = vec![ hashmap! { "searchRules" => json!({"sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": ["color = blue"]}, "products": {"filter": "doggos.age <= 5"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, // filter on sales should override filters on * hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": ["color = blue"]}, "products": {"filter": ["doggos.age <= 5"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": ["color = blue"]}, "products": {"filter": ["doggos.age <= 5"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_authorized_multiple_search!(both_tenant_tokens, {}, {}, 3, 2); } #[actix_rt::test] async fn filter_single_search_authorized_filter_token() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {"filter": "color = blue"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": "color = blue"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": {"filter": ["color = blue"]}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": ["color = blue"]}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, // filter on sales should override filters on * hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": "color = blue"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": "color = blue"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": ["color = blue"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": ["color = blue"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sal*": {"filter": ["color = blue"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_authorized_single_search!(tenant_tokens, "color = yellow", 1); } #[actix_rt::test] async fn filter_multi_search_authorized_filter_token() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {"filter": ["color = blue"]}, "products": {"filter": ["doggos.age <= 5"]}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, // filter on sales should override filters on * hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": "color = blue"}, "products": {"filter": "doggos.age <= 5"} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {"filter": "color = green"}, "sales": {"filter": ["color = blue"]}, "products": {"filter": ["doggos.age <= 5"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sales": {"filter": ["color = blue"]}, "products": {"filter": ["doggos.age <= 5"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({ "*": {}, "sal*": {"filter": ["color = blue"]}, "pro*": {"filter": ["doggos.age <= 5"]} }), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_authorized_multiple_search!(tenant_tokens, "color = yellow", "doggos.age > 4", 1, 1); } /// Tests that those Tenant Token are incompatible with the REFUSED_KEYS defined above. #[actix_rt::test] async fn error_single_search_token_forbidden_parent_key() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sali*", "s*", "sales*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_forbidden_single_search!( tenant_tokens, SINGLE_REFUSED_KEYS, vec![vec![None; 7], vec![None; 7], vec![Some(0); 7], vec![Some(0); 7], vec![Some(0); 7]] ); } /// Tests that those Tenant Token are incompatible with the REFUSED_KEYS defined above. #[actix_rt::test] async fn error_multi_search_token_forbidden_parent_key() { let tenant_tokens = vec![ hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}, "products": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": Value::Null, "products": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales", "products"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sali*", "s*", "sales*", "pro*", "proa*", "products*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, ]; compute_forbidden_multiple_search!( tenant_tokens, BOTH_REFUSED_KEYS, vec![ vec![None; 7], vec![None; 7], vec![Some(1); 7], vec![Some(1); 7], vec![Some(1); 7], vec![Some(0); 7], vec![Some(0); 7], vec![Some(0); 7] ] ); } #[actix_rt::test] async fn error_single_search_forbidden_token() { let tenant_tokens = vec![ // bad index hashmap! { "searchRules" => json!({"products": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["products"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"products": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"products": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["products"]), "exp" => Value::Null }, // expired token hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales"]), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, ]; let failed_query_indexes: Vec<_> = std::iter::repeat(Some(0)).take(5).chain(std::iter::repeat(None).take(6)).collect(); let failed_query_indexes = vec![failed_query_indexes; ACCEPTED_KEYS_SINGLE.len()]; compute_forbidden_single_search!(tenant_tokens, ACCEPTED_KEYS_SINGLE, failed_query_indexes); } #[actix_rt::test] async fn error_multi_search_forbidden_token() { let tenant_tokens = vec![ // bad index hashmap! { "searchRules" => json!({"products": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["products"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"products": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"products": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["products"]), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}}), "exp" => Value::Null }, hashmap! { "searchRules" => json!({"sales": Value::Null}), "exp" => Value::Null }, hashmap! { "searchRules" => json!(["sales"]), "exp" => Value::Null }, // expired token hashmap! { "searchRules" => json!({"*": {}}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"*": Value::Null}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": {}, "products": {}}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!({"sales": Value::Null, "products": {}}), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, hashmap! { "searchRules" => json!(["sales", "products"]), "exp" => json!((OffsetDateTime::now_utc() - Duration::hours(1)).unix_timestamp()) }, ]; let failed_query_indexes: Vec<_> = std::iter::repeat(Some(0)) .take(5) .chain(std::iter::repeat(Some(1)).take(5)) .chain(std::iter::repeat(None).take(6)) .collect(); let failed_query_indexes = vec![failed_query_indexes; ACCEPTED_KEYS_BOTH.len()]; compute_forbidden_multiple_search!(tenant_tokens, ACCEPTED_KEYS_BOTH, failed_query_indexes); } #[actix_rt::test] async fn error_access_expired_parent_key() { use std::{thread, time}; let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); let content = json!({ "indexes": ["*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::seconds(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; assert_eq!(code, 201); assert!(response["key"].is_string()); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); let tenant_token = hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }; let web_token = generate_tenant_token(uid, key, tenant_token); server.use_api_key(&web_token); // test search request while parent_key is not expired let (response, code) = server .multi_search(json!({"queries" : [{"indexUid": "sales"}, {"indexUid": "products"}]})) .await; 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 .multi_search(json!({"queries" : [{"indexUid": "sales"}, {"indexUid": "products"}]})) .await; assert_eq!(response, invalid_response(None)); assert_eq!(code, 403); } #[actix_rt::test] async fn error_access_modified_token() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); let content = json!({ "indexes": ["*"], "actions": ["*"], "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; assert_eq!(code, 201); assert!(response["key"].is_string()); let key = response["key"].as_str().unwrap(); let uid = response["uid"].as_str().unwrap(); let tenant_token = hashmap! { "searchRules" => json!(["products"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }; let web_token = generate_tenant_token(uid, key, tenant_token); server.use_api_key(&web_token); // test search request while web_token is valid let (response, code) = server.multi_search(json!({"queries" : [{"indexUid": "products"}]})).await; assert_ne!(response, invalid_response(Some(0))); assert_ne!(code, 403); let tenant_token = hashmap! { "searchRules" => json!(["*"]), "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }; let alt = generate_tenant_token(uid, key, tenant_token); let altered_token = [ web_token.split('.').next().unwrap(), alt.split('.').nth(1).unwrap(), web_token.split('.').nth(2).unwrap(), ] .join("."); server.use_api_key(&altered_token); let (response, code) = server.multi_search(json!({"queries" : [{"indexUid": "products"}]})).await; assert_eq!(response, invalid_response(None)); assert_eq!(code, 403); }