mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-11-26 06:44:27 +01:00
2099: feat(analytics): Set the timestamp of the aggregated event as the first aggregate r=MarinPostma a=irevoire 2108: meta(auth): Enhance tests on authorization r=MarinPostma a=ManyTheFish Enhance auth tests in order to be able to add new actions without changing tests. Helping #2080 Co-authored-by: Tamo <tamo@meilisearch.com> Co-authored-by: ManyTheFish <many@meilisearch.com>
This commit is contained in:
commit
7e2f6063ae
@ -6,6 +6,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use actix_web::http::header::USER_AGENT;
|
use actix_web::http::header::USER_AGENT;
|
||||||
use actix_web::HttpRequest;
|
use actix_web::HttpRequest;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use http::header::CONTENT_TYPE;
|
use http::header::CONTENT_TYPE;
|
||||||
use meilisearch_lib::index::{SearchQuery, SearchResult};
|
use meilisearch_lib::index::{SearchQuery, SearchResult};
|
||||||
use meilisearch_lib::index_controller::Stats;
|
use meilisearch_lib::index_controller::Stats;
|
||||||
@ -301,6 +302,8 @@ impl Segment {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SearchAggregator {
|
pub struct SearchAggregator {
|
||||||
|
timestamp: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
// context
|
// context
|
||||||
user_agents: HashSet<String>,
|
user_agents: HashSet<String>,
|
||||||
|
|
||||||
@ -336,6 +339,8 @@ pub struct SearchAggregator {
|
|||||||
impl SearchAggregator {
|
impl SearchAggregator {
|
||||||
pub fn from_query(query: &SearchQuery, request: &HttpRequest) -> Self {
|
pub fn from_query(query: &SearchQuery, request: &HttpRequest) -> Self {
|
||||||
let mut ret = Self::default();
|
let mut ret = Self::default();
|
||||||
|
ret.timestamp = Some(chrono::offset::Utc::now());
|
||||||
|
|
||||||
ret.total_received = 1;
|
ret.total_received = 1;
|
||||||
ret.user_agents = extract_user_agents(request).into_iter().collect();
|
ret.user_agents = extract_user_agents(request).into_iter().collect();
|
||||||
|
|
||||||
@ -389,6 +394,10 @@ impl SearchAggregator {
|
|||||||
|
|
||||||
/// Aggregate one [SearchAggregator] into another.
|
/// Aggregate one [SearchAggregator] into another.
|
||||||
pub fn aggregate(&mut self, mut other: Self) {
|
pub fn aggregate(&mut self, mut other: Self) {
|
||||||
|
if self.timestamp.is_none() {
|
||||||
|
self.timestamp = other.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
// context
|
// context
|
||||||
for user_agent in other.user_agents.into_iter() {
|
for user_agent in other.user_agents.into_iter() {
|
||||||
self.user_agents.insert(user_agent);
|
self.user_agents.insert(user_agent);
|
||||||
@ -462,6 +471,7 @@ impl SearchAggregator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Some(Track {
|
Some(Track {
|
||||||
|
timestamp: self.timestamp,
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
event: event_name.to_string(),
|
event: event_name.to_string(),
|
||||||
properties,
|
properties,
|
||||||
@ -473,6 +483,8 @@ impl SearchAggregator {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DocumentsAggregator {
|
pub struct DocumentsAggregator {
|
||||||
|
timestamp: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
// set to true when at least one request was received
|
// set to true when at least one request was received
|
||||||
updated: bool,
|
updated: bool,
|
||||||
|
|
||||||
@ -491,6 +503,7 @@ impl DocumentsAggregator {
|
|||||||
request: &HttpRequest,
|
request: &HttpRequest,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut ret = Self::default();
|
let mut ret = Self::default();
|
||||||
|
ret.timestamp = Some(chrono::offset::Utc::now());
|
||||||
|
|
||||||
ret.updated = true;
|
ret.updated = true;
|
||||||
ret.user_agents = extract_user_agents(request).into_iter().collect();
|
ret.user_agents = extract_user_agents(request).into_iter().collect();
|
||||||
@ -511,6 +524,10 @@ impl DocumentsAggregator {
|
|||||||
|
|
||||||
/// Aggregate one [DocumentsAggregator] into another.
|
/// Aggregate one [DocumentsAggregator] into another.
|
||||||
pub fn aggregate(&mut self, other: Self) {
|
pub fn aggregate(&mut self, other: Self) {
|
||||||
|
if self.timestamp.is_none() {
|
||||||
|
self.timestamp = other.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
self.updated |= other.updated;
|
self.updated |= other.updated;
|
||||||
// we can't create a union because there is no `into_union` method
|
// we can't create a union because there is no `into_union` method
|
||||||
for user_agent in other.user_agents.into_iter() {
|
for user_agent in other.user_agents.into_iter() {
|
||||||
@ -537,6 +554,7 @@ impl DocumentsAggregator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Some(Track {
|
Some(Track {
|
||||||
|
timestamp: self.timestamp,
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
event: event_name.to_string(),
|
event: event_name.to_string(),
|
||||||
properties,
|
properties,
|
||||||
|
@ -1,56 +1,61 @@
|
|||||||
use crate::common::Server;
|
use crate::common::Server;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use maplit::hashmap;
|
use maplit::{hashmap, hashset};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), &'static str>> =
|
static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'static str>>> =
|
||||||
Lazy::new(|| {
|
Lazy::new(|| {
|
||||||
hashmap! {
|
hashmap! {
|
||||||
("POST", "/indexes/products/search") => "search",
|
("POST", "/indexes/products/search") => hashset!{"search", "*"},
|
||||||
("GET", "/indexes/products/search") => "search",
|
("GET", "/indexes/products/search") => hashset!{"search", "*"},
|
||||||
("POST", "/indexes/products/documents") => "documents.add",
|
("POST", "/indexes/products/documents") => hashset!{"documents.add", "*"},
|
||||||
("GET", "/indexes/products/documents") => "documents.get",
|
("GET", "/indexes/products/documents") => hashset!{"documents.get", "*"},
|
||||||
("GET", "/indexes/products/documents/0") => "documents.get",
|
("GET", "/indexes/products/documents/0") => hashset!{"documents.get", "*"},
|
||||||
("DELETE", "/indexes/products/documents/0") => "documents.delete",
|
("DELETE", "/indexes/products/documents/0") => hashset!{"documents.delete", "*"},
|
||||||
("GET", "/tasks") => "tasks.get",
|
("GET", "/tasks") => hashset!{"tasks.get", "*"},
|
||||||
("GET", "/indexes/products/tasks") => "tasks.get",
|
("GET", "/indexes/products/tasks") => hashset!{"tasks.get", "*"},
|
||||||
("GET", "/indexes/products/tasks/0") => "tasks.get",
|
("GET", "/indexes/products/tasks/0") => hashset!{"tasks.get", "*"},
|
||||||
("PUT", "/indexes/products/") => "indexes.update",
|
("PUT", "/indexes/products/") => hashset!{"indexes.update", "*"},
|
||||||
("GET", "/indexes/products/") => "indexes.get",
|
("GET", "/indexes/products/") => hashset!{"indexes.get", "*"},
|
||||||
("DELETE", "/indexes/products/") => "indexes.delete",
|
("DELETE", "/indexes/products/") => hashset!{"indexes.delete", "*"},
|
||||||
("POST", "/indexes") => "indexes.create",
|
("POST", "/indexes") => hashset!{"indexes.create", "*"},
|
||||||
("GET", "/indexes") => "indexes.get",
|
("GET", "/indexes") => hashset!{"indexes.get", "*"},
|
||||||
("GET", "/indexes/products/settings") => "settings.get",
|
("GET", "/indexes/products/settings") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/displayed-attributes") => "settings.get",
|
("GET", "/indexes/products/settings/displayed-attributes") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/distinct-attribute") => "settings.get",
|
("GET", "/indexes/products/settings/distinct-attribute") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/filterable-attributes") => "settings.get",
|
("GET", "/indexes/products/settings/filterable-attributes") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/ranking-rules") => "settings.get",
|
("GET", "/indexes/products/settings/ranking-rules") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/searchable-attributes") => "settings.get",
|
("GET", "/indexes/products/settings/searchable-attributes") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/sortable-attributes") => "settings.get",
|
("GET", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/stop-words") => "settings.get",
|
("GET", "/indexes/products/settings/stop-words") => hashset!{"settings.get", "*"},
|
||||||
("GET", "/indexes/products/settings/synonyms") => "settings.get",
|
("GET", "/indexes/products/settings/synonyms") => hashset!{"settings.get", "*"},
|
||||||
("DELETE", "/indexes/products/settings") => "settings.update",
|
("DELETE", "/indexes/products/settings") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings") => "settings.update",
|
("POST", "/indexes/products/settings") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/displayed-attributes") => "settings.update",
|
("POST", "/indexes/products/settings/displayed-attributes") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/distinct-attribute") => "settings.update",
|
("POST", "/indexes/products/settings/distinct-attribute") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/filterable-attributes") => "settings.update",
|
("POST", "/indexes/products/settings/filterable-attributes") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/ranking-rules") => "settings.update",
|
("POST", "/indexes/products/settings/ranking-rules") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/searchable-attributes") => "settings.update",
|
("POST", "/indexes/products/settings/searchable-attributes") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/sortable-attributes") => "settings.update",
|
("POST", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/stop-words") => "settings.update",
|
("POST", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "*"},
|
||||||
("POST", "/indexes/products/settings/synonyms") => "settings.update",
|
("POST", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "*"},
|
||||||
("GET", "/indexes/products/stats") => "stats.get",
|
("GET", "/indexes/products/stats") => hashset!{"stats.get", "*"},
|
||||||
("GET", "/stats") => "stats.get",
|
("GET", "/stats") => hashset!{"stats.get", "*"},
|
||||||
("POST", "/dumps") => "dumps.create",
|
("POST", "/dumps") => hashset!{"dumps.create", "*"},
|
||||||
("GET", "/dumps/0/status") => "dumps.get",
|
("GET", "/dumps/0/status") => hashset!{"dumps.get", "*"},
|
||||||
("GET", "/version") => "version",
|
("GET", "/version") => hashset!{"version", "*"},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static ALL_ACTIONS: Lazy<HashSet<&'static str>> =
|
static ALL_ACTIONS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||||
Lazy::new(|| AUTHORIZATIONS.values().cloned().collect());
|
AUTHORIZATIONS
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.reduce(|l, r| l.union(&r).cloned().collect())
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
static INVALID_RESPONSE: Lazy<Value> = Lazy::new(|| {
|
static INVALID_RESPONSE: Lazy<Value> = Lazy::new(|| {
|
||||||
json!({"message": "The provided API key is invalid.",
|
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.
|
// Patch API key letting all rights but the needed one.
|
||||||
let content = json!({
|
let content = json!({
|
||||||
"actions": ALL_ACTIONS.iter().cloned().filter(|a| a != action).collect::<Vec<_>>(),
|
"actions": ALL_ACTIONS.difference(action).collect::<Vec<_>>(),
|
||||||
});
|
});
|
||||||
let (_, code) = server.patch_api_key(&key, content).await;
|
let (_, code) = server.patch_api_key(&key, content).await;
|
||||||
assert_eq!(code, 200);
|
assert_eq!(code, 200);
|
||||||
@ -179,7 +184,8 @@ async fn access_authorized_restricted_index() {
|
|||||||
let key = response["key"].as_str().unwrap();
|
let key = response["key"].as_str().unwrap();
|
||||||
server.use_api_key(&key);
|
server.use_api_key(&key);
|
||||||
|
|
||||||
for ((method, route), action) in AUTHORIZATIONS.iter() {
|
for ((method, route), actions) in AUTHORIZATIONS.iter() {
|
||||||
|
for action in actions {
|
||||||
// Patch API key letting only the needed action.
|
// Patch API key letting only the needed action.
|
||||||
let content = json!({
|
let content = json!({
|
||||||
"actions": [action],
|
"actions": [action],
|
||||||
@ -194,21 +200,7 @@ async fn access_authorized_restricted_index() {
|
|||||||
|
|
||||||
assert_ne!(response, INVALID_RESPONSE.clone());
|
assert_ne!(response, INVALID_RESPONSE.clone());
|
||||||
assert_ne!(code, 403);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +223,8 @@ async fn access_authorized_no_index_restriction() {
|
|||||||
let key = response["key"].as_str().unwrap();
|
let key = response["key"].as_str().unwrap();
|
||||||
server.use_api_key(&key);
|
server.use_api_key(&key);
|
||||||
|
|
||||||
for ((method, route), action) in AUTHORIZATIONS.iter() {
|
for ((method, route), actions) in AUTHORIZATIONS.iter() {
|
||||||
|
for action in actions {
|
||||||
server.use_api_key("MASTER_KEY");
|
server.use_api_key("MASTER_KEY");
|
||||||
|
|
||||||
// Patch API key letting only the needed action.
|
// Patch API key letting only the needed action.
|
||||||
@ -246,21 +239,7 @@ async fn access_authorized_no_index_restriction() {
|
|||||||
|
|
||||||
assert_ne!(response, INVALID_RESPONSE.clone());
|
assert_ne!(response, INVALID_RESPONSE.clone());
|
||||||
assert_ne!(code, 403);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,7 +493,8 @@ async fn error_creating_index_without_action() {
|
|||||||
// create key with access on all indexes.
|
// create key with access on all indexes.
|
||||||
let content = json!({
|
let content = json!({
|
||||||
"indexes": ["*"],
|
"indexes": ["*"],
|
||||||
"actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "indexes.create").collect::<Vec<_>>(),
|
// 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::<Vec<_>>(),
|
||||||
"expiresAt": "2050-11-13T00:00:00Z"
|
"expiresAt": "2050-11-13T00:00:00Z"
|
||||||
});
|
});
|
||||||
let (response, code) = server.add_api_key(content).await;
|
let (response, code) = server.add_api_key(content).await;
|
||||||
|
Loading…
Reference in New Issue
Block a user