diff --git a/meilisearch-http/src/analytics/segment_analytics.rs b/meilisearch-http/src/analytics/segment_analytics.rs index 86be0f432..597da7523 100644 --- a/meilisearch-http/src/analytics/segment_analytics.rs +++ b/meilisearch-http/src/analytics/segment_analytics.rs @@ -6,6 +6,7 @@ use std::time::{Duration, Instant}; use actix_web::http::header::USER_AGENT; use actix_web::HttpRequest; +use chrono::{DateTime, Utc}; use http::header::CONTENT_TYPE; use meilisearch_lib::index::{SearchQuery, SearchResult}; use meilisearch_lib::index_controller::Stats; @@ -301,6 +302,8 @@ impl Segment { #[derive(Default)] pub struct SearchAggregator { + timestamp: Option>, + // context user_agents: HashSet, @@ -336,6 +339,8 @@ pub struct SearchAggregator { impl SearchAggregator { pub fn from_query(query: &SearchQuery, request: &HttpRequest) -> Self { let mut ret = Self::default(); + ret.timestamp = Some(chrono::offset::Utc::now()); + ret.total_received = 1; ret.user_agents = extract_user_agents(request).into_iter().collect(); @@ -389,6 +394,10 @@ impl SearchAggregator { /// Aggregate one [SearchAggregator] into another. pub fn aggregate(&mut self, mut other: Self) { + if self.timestamp.is_none() { + self.timestamp = other.timestamp; + } + // context for user_agent in other.user_agents.into_iter() { self.user_agents.insert(user_agent); @@ -462,6 +471,7 @@ impl SearchAggregator { }); Some(Track { + timestamp: self.timestamp, user: user.clone(), event: event_name.to_string(), properties, @@ -473,6 +483,8 @@ impl SearchAggregator { #[derive(Default)] pub struct DocumentsAggregator { + timestamp: Option>, + // set to true when at least one request was received updated: bool, @@ -491,6 +503,7 @@ impl DocumentsAggregator { request: &HttpRequest, ) -> Self { let mut ret = Self::default(); + ret.timestamp = Some(chrono::offset::Utc::now()); ret.updated = true; ret.user_agents = extract_user_agents(request).into_iter().collect(); @@ -511,6 +524,10 @@ impl DocumentsAggregator { /// Aggregate one [DocumentsAggregator] into another. pub fn aggregate(&mut self, other: Self) { + if self.timestamp.is_none() { + self.timestamp = other.timestamp; + } + self.updated |= other.updated; // we can't create a union because there is no `into_union` method for user_agent in other.user_agents.into_iter() { @@ -537,6 +554,7 @@ impl DocumentsAggregator { }); Some(Track { + timestamp: self.timestamp, user: user.clone(), event: event_name.to_string(), properties, diff --git a/meilisearch-http/tests/auth/authorization.rs b/meilisearch-http/tests/auth/authorization.rs index 98e0a1a1d..7ea232f0b 100644 --- a/meilisearch-http/tests/auth/authorization.rs +++ b/meilisearch-http/tests/auth/authorization.rs @@ -1,56 +1,61 @@ use crate::common::Server; use chrono::{Duration, Utc}; -use maplit::hashmap; +use maplit::{hashmap, hashset}; use once_cell::sync::Lazy; use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; -static AUTHORIZATIONS: Lazy> = +static AUTHORIZATIONS: Lazy>> = Lazy::new(|| { hashmap! { - ("POST", "/indexes/products/search") => "search", - ("GET", "/indexes/products/search") => "search", - ("POST", "/indexes/products/documents") => "documents.add", - ("GET", "/indexes/products/documents") => "documents.get", - ("GET", "/indexes/products/documents/0") => "documents.get", - ("DELETE", "/indexes/products/documents/0") => "documents.delete", - ("GET", "/tasks") => "tasks.get", - ("GET", "/indexes/products/tasks") => "tasks.get", - ("GET", "/indexes/products/tasks/0") => "tasks.get", - ("PUT", "/indexes/products/") => "indexes.update", - ("GET", "/indexes/products/") => "indexes.get", - ("DELETE", "/indexes/products/") => "indexes.delete", - ("POST", "/indexes") => "indexes.create", - ("GET", "/indexes") => "indexes.get", - ("GET", "/indexes/products/settings") => "settings.get", - ("GET", "/indexes/products/settings/displayed-attributes") => "settings.get", - ("GET", "/indexes/products/settings/distinct-attribute") => "settings.get", - ("GET", "/indexes/products/settings/filterable-attributes") => "settings.get", - ("GET", "/indexes/products/settings/ranking-rules") => "settings.get", - ("GET", "/indexes/products/settings/searchable-attributes") => "settings.get", - ("GET", "/indexes/products/settings/sortable-attributes") => "settings.get", - ("GET", "/indexes/products/settings/stop-words") => "settings.get", - ("GET", "/indexes/products/settings/synonyms") => "settings.get", - ("DELETE", "/indexes/products/settings") => "settings.update", - ("POST", "/indexes/products/settings") => "settings.update", - ("POST", "/indexes/products/settings/displayed-attributes") => "settings.update", - ("POST", "/indexes/products/settings/distinct-attribute") => "settings.update", - ("POST", "/indexes/products/settings/filterable-attributes") => "settings.update", - ("POST", "/indexes/products/settings/ranking-rules") => "settings.update", - ("POST", "/indexes/products/settings/searchable-attributes") => "settings.update", - ("POST", "/indexes/products/settings/sortable-attributes") => "settings.update", - ("POST", "/indexes/products/settings/stop-words") => "settings.update", - ("POST", "/indexes/products/settings/synonyms") => "settings.update", - ("GET", "/indexes/products/stats") => "stats.get", - ("GET", "/stats") => "stats.get", - ("POST", "/dumps") => "dumps.create", - ("GET", "/dumps/0/status") => "dumps.get", - ("GET", "/version") => "version", + ("POST", "/indexes/products/search") => hashset!{"search", "*"}, + ("GET", "/indexes/products/search") => hashset!{"search", "*"}, + ("POST", "/indexes/products/documents") => hashset!{"documents.add", "*"}, + ("GET", "/indexes/products/documents") => hashset!{"documents.get", "*"}, + ("GET", "/indexes/products/documents/0") => hashset!{"documents.get", "*"}, + ("DELETE", "/indexes/products/documents/0") => hashset!{"documents.delete", "*"}, + ("GET", "/tasks") => hashset!{"tasks.get", "*"}, + ("GET", "/indexes/products/tasks") => hashset!{"tasks.get", "*"}, + ("GET", "/indexes/products/tasks/0") => hashset!{"tasks.get", "*"}, + ("PUT", "/indexes/products/") => hashset!{"indexes.update", "*"}, + ("GET", "/indexes/products/") => hashset!{"indexes.get", "*"}, + ("DELETE", "/indexes/products/") => hashset!{"indexes.delete", "*"}, + ("POST", "/indexes") => hashset!{"indexes.create", "*"}, + ("GET", "/indexes") => hashset!{"indexes.get", "*"}, + ("GET", "/indexes/products/settings") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/displayed-attributes") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/distinct-attribute") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/filterable-attributes") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/ranking-rules") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/searchable-attributes") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/stop-words") => hashset!{"settings.get", "*"}, + ("GET", "/indexes/products/settings/synonyms") => hashset!{"settings.get", "*"}, + ("DELETE", "/indexes/products/settings") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/displayed-attributes") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/distinct-attribute") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/filterable-attributes") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/ranking-rules") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/searchable-attributes") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "*"}, + ("POST", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "*"}, + ("GET", "/indexes/products/stats") => hashset!{"stats.get", "*"}, + ("GET", "/stats") => hashset!{"stats.get", "*"}, + ("POST", "/dumps") => hashset!{"dumps.create", "*"}, + ("GET", "/dumps/0/status") => hashset!{"dumps.get", "*"}, + ("GET", "/version") => hashset!{"version", "*"}, } }); -static ALL_ACTIONS: Lazy> = - Lazy::new(|| AUTHORIZATIONS.values().cloned().collect()); +static ALL_ACTIONS: Lazy> = Lazy::new(|| { + AUTHORIZATIONS + .values() + .cloned() + .reduce(|l, r| l.union(&r).cloned().collect()) + .unwrap() +}); static INVALID_RESPONSE: Lazy = Lazy::new(|| { json!({"message": "The provided API key is invalid.", @@ -147,7 +152,7 @@ async fn error_access_unauthorized_action() { // Patch API key letting all rights but the needed one. let content = json!({ - "actions": ALL_ACTIONS.iter().cloned().filter(|a| a != action).collect::>(), + "actions": ALL_ACTIONS.difference(action).collect::>(), }); let (_, code) = server.patch_api_key(&key, content).await; assert_eq!(code, 200); @@ -179,36 +184,23 @@ async fn access_authorized_restricted_index() { let key = response["key"].as_str().unwrap(); server.use_api_key(&key); - for ((method, route), action) in AUTHORIZATIONS.iter() { - // Patch API key letting only the needed action. - let content = json!({ - "actions": [action], - }); + for ((method, route), actions) in AUTHORIZATIONS.iter() { + for action in actions { + // Patch API key letting only the needed action. + let content = json!({ + "actions": [action], + }); - server.use_api_key("MASTER_KEY"); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + server.use_api_key("MASTER_KEY"); + let (_, code) = server.patch_api_key(&key, content).await; + assert_eq!(code, 200); - server.use_api_key(&key); - let (response, code) = server.dummy_request(method, route).await; + server.use_api_key(&key); + let (response, code) = server.dummy_request(method, route).await; - assert_ne!(response, INVALID_RESPONSE.clone()); - assert_ne!(code, 403); - - // Patch API key using action all action. - let content = json!({ - "actions": ["*"], - }); - - server.use_api_key("MASTER_KEY"); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); - - server.use_api_key(&key); - let (response, code) = server.dummy_request(method, route).await; - - assert_ne!(response, INVALID_RESPONSE.clone()); - assert_ne!(code, 403); + assert_ne!(response, INVALID_RESPONSE.clone()); + assert_ne!(code, 403); + } } } @@ -231,36 +223,23 @@ async fn access_authorized_no_index_restriction() { let key = response["key"].as_str().unwrap(); server.use_api_key(&key); - for ((method, route), action) in AUTHORIZATIONS.iter() { - server.use_api_key("MASTER_KEY"); + for ((method, route), actions) in AUTHORIZATIONS.iter() { + for action in actions { + server.use_api_key("MASTER_KEY"); - // Patch API key letting only the needed action. - let content = json!({ - "actions": [action], - }); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + // Patch API key letting only the needed action. + let content = json!({ + "actions": [action], + }); + let (_, code) = server.patch_api_key(&key, content).await; + assert_eq!(code, 200); - server.use_api_key(&key); - let (response, code) = server.dummy_request(method, route).await; + server.use_api_key(&key); + let (response, code) = server.dummy_request(method, route).await; - assert_ne!(response, INVALID_RESPONSE.clone()); - assert_ne!(code, 403); - - // Patch API key using action all action. - let content = json!({ - "actions": ["*"], - }); - - server.use_api_key("MASTER_KEY"); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); - - server.use_api_key(&key); - let (response, code) = server.dummy_request(method, route).await; - - assert_ne!(response, INVALID_RESPONSE.clone()); - assert_ne!(code, 403); + assert_ne!(response, INVALID_RESPONSE.clone()); + assert_ne!(code, 403); + } } } @@ -514,7 +493,8 @@ async fn error_creating_index_without_action() { // create key with access on all indexes. let content = json!({ "indexes": ["*"], - "actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "indexes.create").collect::>(), + // 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::>(), "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await;