diff --git a/Cargo.lock b/Cargo.lock index 39eb78987..b72913520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1973,6 +1973,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" name = "meilisearch-auth" version = "0.27.1" dependencies = [ + "base64", "enum-iterator", "meilisearch-error", "milli", @@ -1982,6 +1983,7 @@ dependencies = [ "sha2", "thiserror", "time 0.3.9", + "uuid", ] [[package]] diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index dd12b5b63..29fa78a14 100644 --- a/meilisearch-auth/Cargo.toml +++ b/meilisearch-auth/Cargo.toml @@ -4,6 +4,7 @@ version = "0.27.1" edition = "2021" [dependencies] +base64 = "0.13.0" enum-iterator = "0.7.0" meilisearch-error = { path = "../meilisearch-error" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.28.0" } @@ -13,3 +14,4 @@ serde_json = { version = "1.0.79", features = ["preserve_order"] } sha2 = "0.10.2" thiserror = "1.0.30" time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } +uuid = { version = "0.8.2", features = ["serde", "v4"] } diff --git a/meilisearch-auth/src/action.rs b/meilisearch-auth/src/action.rs index 7ffe9b908..088ad6ba7 100644 --- a/meilisearch-auth/src/action.rs +++ b/meilisearch-auth/src/action.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[repr(u8)] pub enum Action { #[serde(rename = "*")] - All = 0, + All = actions::ALL, #[serde(rename = "search")] Search = actions::SEARCH, #[serde(rename = "documents.add")] @@ -36,13 +36,21 @@ pub enum Action { DumpsGet = actions::DUMPS_GET, #[serde(rename = "version")] Version = actions::VERSION, + #[serde(rename = "keys.create")] + KeysAdd = actions::KEYS_CREATE, + #[serde(rename = "keys.get")] + KeysGet = actions::KEYS_GET, + #[serde(rename = "keys.update")] + KeysUpdate = actions::KEYS_UPDATE, + #[serde(rename = "keys.delete")] + KeysDelete = actions::KEYS_DELETE, } impl Action { pub fn from_repr(repr: u8) -> Option { use actions::*; match repr { - 0 => Some(Self::All), + ALL => Some(Self::All), SEARCH => Some(Self::Search), DOCUMENTS_ADD => Some(Self::DocumentsAdd), DOCUMENTS_GET => Some(Self::DocumentsGet), @@ -58,6 +66,10 @@ impl Action { DUMPS_CREATE => Some(Self::DumpsCreate), DUMPS_GET => Some(Self::DumpsGet), VERSION => Some(Self::Version), + KEYS_CREATE => Some(Self::KeysAdd), + KEYS_GET => Some(Self::KeysGet), + KEYS_UPDATE => Some(Self::KeysUpdate), + KEYS_DELETE => Some(Self::KeysDelete), _otherwise => None, } } @@ -65,7 +77,7 @@ impl Action { pub fn repr(&self) -> u8 { use actions::*; match self { - Self::All => 0, + Self::All => ALL, Self::Search => SEARCH, Self::DocumentsAdd => DOCUMENTS_ADD, Self::DocumentsGet => DOCUMENTS_GET, @@ -81,11 +93,16 @@ impl Action { Self::DumpsCreate => DUMPS_CREATE, Self::DumpsGet => DUMPS_GET, Self::Version => VERSION, + Self::KeysAdd => KEYS_CREATE, + Self::KeysGet => KEYS_GET, + Self::KeysUpdate => KEYS_UPDATE, + Self::KeysDelete => KEYS_DELETE, } } } pub mod actions { + pub(crate) const ALL: u8 = 0; pub const SEARCH: u8 = 1; pub const DOCUMENTS_ADD: u8 = 2; pub const DOCUMENTS_GET: u8 = 3; @@ -101,4 +118,8 @@ pub mod actions { pub const DUMPS_CREATE: u8 = 13; pub const DUMPS_GET: u8 = 14; pub const VERSION: u8 = 15; + pub const KEYS_CREATE: u8 = 16; + pub const KEYS_GET: u8 = 17; + pub const KEYS_UPDATE: u8 = 18; + pub const KEYS_DELETE: u8 = 19; } diff --git a/meilisearch-auth/src/dump.rs b/meilisearch-auth/src/dump.rs index 77a4aa5ca..7e607e574 100644 --- a/meilisearch-auth/src/dump.rs +++ b/meilisearch-auth/src/dump.rs @@ -1,5 +1,6 @@ +use serde_json::Deserializer; + use std::fs::File; -use std::io::BufRead; use std::io::BufReader; use std::io::Write; use std::path::Path; @@ -36,10 +37,9 @@ impl AuthController { return Ok(()); } - let mut reader = BufReader::new(File::open(&keys_file_path)?).lines(); - while let Some(key) = reader.next().transpose()? { - let key = serde_json::from_str(&key)?; - store.put_api_key(key)?; + let reader = BufReader::new(File::open(&keys_file_path)?); + for key in Deserializer::from_reader(reader).into_iter() { + store.put_api_key(key?)?; } Ok(()) diff --git a/meilisearch-auth/src/error.rs b/meilisearch-auth/src/error.rs index 8a87eda27..dbf28b421 100644 --- a/meilisearch-auth/src/error.rs +++ b/meilisearch-auth/src/error.rs @@ -18,8 +18,18 @@ pub enum AuthControllerError { InvalidApiKeyExpiresAt(Value), #[error("`description` field value `{0}` is invalid. It should be a string or specified as a null value.")] InvalidApiKeyDescription(Value), + #[error( + "`name` field value `{0}` is invalid. It should be a string or specified as a null value." + )] + InvalidApiKeyName(Value), + #[error("`uid` field value `{0}` is invalid. It should be a valid UUID v4 string or omitted.")] + InvalidApiKeyUid(Value), #[error("API key `{0}` not found.")] ApiKeyNotFound(String), + #[error("`uid` field value `{0}` is already an existing API key.")] + ApiKeyAlreadyExists(String), + #[error("`{0}` field cannot be modified for the given resource.")] + ImmutableField(String), #[error("Internal error: {0}")] Internal(Box), } @@ -39,7 +49,11 @@ impl ErrorCode for AuthControllerError { Self::InvalidApiKeyIndexes(_) => Code::InvalidApiKeyIndexes, Self::InvalidApiKeyExpiresAt(_) => Code::InvalidApiKeyExpiresAt, Self::InvalidApiKeyDescription(_) => Code::InvalidApiKeyDescription, + Self::InvalidApiKeyName(_) => Code::InvalidApiKeyName, Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound, + Self::InvalidApiKeyUid(_) => Code::InvalidApiKeyUid, + Self::ApiKeyAlreadyExists(_) => Code::ApiKeyAlreadyExists, + Self::ImmutableField(_) => Code::ImmutableField, Self::Internal(_) => Code::Internal, } } diff --git a/meilisearch-auth/src/key.rs b/meilisearch-auth/src/key.rs index 1b06f34be..0e336a7db 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -1,18 +1,21 @@ use crate::action::Action; use crate::error::{AuthControllerError, Result}; -use crate::store::{KeyId, KEY_ID_LENGTH}; -use rand::Rng; +use crate::store::KeyId; + use serde::{Deserialize, Serialize}; use serde_json::{from_value, Value}; use time::format_description::well_known::Rfc3339; use time::macros::{format_description, time}; use time::{Date, OffsetDateTime, PrimitiveDateTime}; +use uuid::Uuid; #[derive(Debug, Deserialize, Serialize)] pub struct Key { #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - pub id: KeyId, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub uid: KeyId, pub actions: Vec, pub indexes: Vec, #[serde(with = "time::serde::rfc3339::option")] @@ -25,16 +28,27 @@ pub struct Key { impl Key { pub fn create_from_value(value: Value) -> Result { - let description = match value.get("description") { - Some(Value::Null) => None, - Some(des) => Some( - from_value(des.clone()) - .map_err(|_| AuthControllerError::InvalidApiKeyDescription(des.clone()))?, - ), - None => None, + let name = match value.get("name") { + None | Some(Value::Null) => None, + Some(des) => from_value(des.clone()) + .map(Some) + .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?, }; - let id = generate_id(); + let description = match value.get("description") { + None | Some(Value::Null) => None, + Some(des) => from_value(des.clone()) + .map(Some) + .map_err(|_| AuthControllerError::InvalidApiKeyDescription(des.clone()))?, + }; + + let uid = value.get("uid").map_or_else( + || Ok(Uuid::new_v4()), + |uid| { + from_value(uid.clone()) + .map_err(|_| AuthControllerError::InvalidApiKeyUid(uid.clone())) + }, + )?; let actions = value .get("actions") @@ -61,8 +75,9 @@ impl Key { let updated_at = created_at; Ok(Self { + name, description, - id, + uid, actions, indexes, expires_at, @@ -78,20 +93,34 @@ impl Key { self.description = des?; } - if let Some(act) = value.get("actions") { - let act = from_value(act.clone()) - .map_err(|_| AuthControllerError::InvalidApiKeyActions(act.clone())); - self.actions = act?; + if let Some(des) = value.get("name") { + let des = from_value(des.clone()) + .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone())); + self.name = des?; } - if let Some(ind) = value.get("indexes") { - let ind = from_value(ind.clone()) - .map_err(|_| AuthControllerError::InvalidApiKeyIndexes(ind.clone())); - self.indexes = ind?; + if value.get("uid").is_some() { + return Err(AuthControllerError::ImmutableField("uid".to_string())); } - if let Some(exp) = value.get("expiresAt") { - self.expires_at = parse_expiration_date(exp)?; + if value.get("actions").is_some() { + return Err(AuthControllerError::ImmutableField("actions".to_string())); + } + + if value.get("indexes").is_some() { + return Err(AuthControllerError::ImmutableField("indexes".to_string())); + } + + if value.get("expiresAt").is_some() { + return Err(AuthControllerError::ImmutableField("expiresAt".to_string())); + } + + if value.get("createdAt").is_some() { + return Err(AuthControllerError::ImmutableField("createdAt".to_string())); + } + + if value.get("updatedAt").is_some() { + return Err(AuthControllerError::ImmutableField("updatedAt".to_string())); } self.updated_at = OffsetDateTime::now_utc(); @@ -101,9 +130,11 @@ impl Key { pub(crate) fn default_admin() -> Self { let now = OffsetDateTime::now_utc(); + let uid = Uuid::new_v4(); Self { - description: Some("Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)".to_string()), - id: generate_id(), + name: Some("Default Admin API Key".to_string()), + description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()), + uid, actions: vec![Action::All], indexes: vec!["*".to_string()], expires_at: None, @@ -114,11 +145,11 @@ impl Key { pub(crate) fn default_search() -> Self { let now = OffsetDateTime::now_utc(); + let uid = Uuid::new_v4(); Self { - description: Some( - "Default Search API Key (Use it to search from the frontend)".to_string(), - ), - id: generate_id(), + name: Some("Default Search API Key".to_string()), + description: Some("Use it to search from the frontend".to_string()), + uid, actions: vec![Action::Search], indexes: vec!["*".to_string()], expires_at: None, @@ -128,19 +159,6 @@ impl Key { } } -/// Generate a printable key of 64 characters using thread_rng. -fn generate_id() -> [u8; KEY_ID_LENGTH] { - const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - let mut rng = rand::thread_rng(); - let mut bytes = [0; KEY_ID_LENGTH]; - for byte in bytes.iter_mut() { - *byte = CHARSET[rng.gen_range(0..CHARSET.len())]; - } - - bytes -} - fn parse_expiration_date(value: &Value) -> Result> { match value { Value::String(string) => OffsetDateTime::parse(string, &Rfc3339) diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 22263735e..e41fd92f4 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -6,17 +6,17 @@ mod store; use std::collections::{HashMap, HashSet}; use std::path::Path; -use std::str::from_utf8; use std::sync::Arc; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha2::{Digest, Sha256}; use time::OffsetDateTime; +use uuid::Uuid; pub use action::{actions, Action}; use error::{AuthControllerError, Result}; pub use key::Key; +use store::generate_key_as_base64; pub use store::open_auth_store_env; use store::HeedAuthStore; @@ -42,62 +42,75 @@ impl AuthController { pub fn create_key(&self, value: Value) -> Result { let key = Key::create_from_value(value)?; - self.store.put_api_key(key) + match self.store.get_api_key(key.uid)? { + Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists( + key.uid.to_string(), + )), + None => self.store.put_api_key(key), + } } - pub fn update_key(&self, key: impl AsRef, value: Value) -> Result { - let mut key = self.get_key(key)?; + pub fn update_key(&self, uid: Uuid, value: Value) -> Result { + let mut key = self.get_key(uid)?; key.update_from_value(value)?; self.store.put_api_key(key) } - pub fn get_key(&self, key: impl AsRef) -> Result { + pub fn get_key(&self, uid: Uuid) -> Result { self.store - .get_api_key(&key)? - .ok_or_else(|| AuthControllerError::ApiKeyNotFound(key.as_ref().to_string())) + .get_api_key(uid)? + .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string())) + } + + pub fn get_optional_uid_from_encoded_key(&self, encoded_key: &[u8]) -> Result> { + match &self.master_key { + Some(master_key) => self + .store + .get_uid_from_encoded_key(encoded_key, master_key.as_bytes()), + None => Ok(None), + } + } + + pub fn get_uid_from_encoded_key(&self, encoded_key: &str) -> Result { + self.get_optional_uid_from_encoded_key(encoded_key.as_bytes())? + .ok_or_else(|| AuthControllerError::ApiKeyNotFound(encoded_key.to_string())) } pub fn get_key_filters( &self, - key: impl AsRef, + uid: Uuid, search_rules: Option, ) -> Result { let mut filters = AuthFilter::default(); - if self - .master_key - .as_ref() - .map_or(false, |master_key| master_key != key.as_ref()) - { - let key = self - .store - .get_api_key(&key)? - .ok_or_else(|| AuthControllerError::ApiKeyNotFound(key.as_ref().to_string()))?; + let key = self + .store + .get_api_key(uid)? + .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?; - if !key.indexes.iter().any(|i| i.as_str() == "*") { - filters.search_rules = match search_rules { - // Intersect search_rules with parent key authorized indexes. - Some(search_rules) => SearchRules::Map( - key.indexes - .into_iter() - .filter_map(|index| { - search_rules - .get_index_search_rules(&index) - .map(|index_search_rules| (index, Some(index_search_rules))) - }) - .collect(), - ), - None => SearchRules::Set(key.indexes.into_iter().collect()), - }; - } else if let Some(search_rules) = search_rules { - filters.search_rules = search_rules; - } - - filters.allow_index_creation = key - .actions - .iter() - .any(|&action| action == Action::IndexesAdd || action == Action::All); + if !key.indexes.iter().any(|i| i.as_str() == "*") { + filters.search_rules = match search_rules { + // Intersect search_rules with parent key authorized indexes. + Some(search_rules) => SearchRules::Map( + key.indexes + .into_iter() + .filter_map(|index| { + search_rules + .get_index_search_rules(&index) + .map(|index_search_rules| (index, Some(index_search_rules))) + }) + .collect(), + ), + None => SearchRules::Set(key.indexes.into_iter().collect()), + }; + } else if let Some(search_rules) = search_rules { + filters.search_rules = search_rules; } + filters.allow_index_creation = key + .actions + .iter() + .any(|&action| action == Action::IndexesAdd || action == Action::All); + Ok(filters) } @@ -105,13 +118,11 @@ impl AuthController { self.store.list_api_keys() } - pub fn delete_key(&self, key: impl AsRef) -> Result<()> { - if self.store.delete_api_key(&key)? { + pub fn delete_key(&self, uid: Uuid) -> Result<()> { + if self.store.delete_api_key(uid)? { Ok(()) } else { - Err(AuthControllerError::ApiKeyNotFound( - key.as_ref().to_string(), - )) + Err(AuthControllerError::ApiKeyNotFound(uid.to_string())) } } @@ -121,32 +132,32 @@ impl AuthController { /// Generate a valid key from a key id using the current master key. /// Returns None if no master key has been set. - pub fn generate_key(&self, id: &str) -> Option { + pub fn generate_key(&self, uid: Uuid) -> Option { self.master_key .as_ref() - .map(|master_key| generate_key(master_key.as_bytes(), id)) + .map(|master_key| generate_key_as_base64(uid.as_bytes(), master_key.as_bytes())) } /// Check if the provided key is authorized to make a specific action /// without checking if the key is valid. pub fn is_key_authorized( &self, - key: &[u8], + uid: Uuid, action: Action, index: Option<&str>, ) -> Result { match self .store // check if the key has access to all indexes. - .get_expiration_date(key, action, None)? + .get_expiration_date(uid, action, None)? .or(match index { // else check if the key has access to the requested index. Some(index) => { self.store - .get_expiration_date(key, action, Some(index.as_bytes()))? + .get_expiration_date(uid, action, Some(index.as_bytes()))? } // or to any index if no index has been requested. - None => self.store.prefix_first_expiration_date(key, action)?, + None => self.store.prefix_first_expiration_date(uid, action)?, }) { // check expiration date. Some(Some(exp)) => Ok(OffsetDateTime::now_utc() < exp), @@ -156,29 +167,6 @@ impl AuthController { None => Ok(false), } } - - /// Check if the provided key is valid - /// without checking if the key is authorized to make a specific action. - pub fn is_key_valid(&self, key: &[u8]) -> Result { - if let Some(id) = self.store.get_key_id(key) { - let id = from_utf8(&id)?; - if let Some(generated) = self.generate_key(id) { - return Ok(generated.as_bytes() == key); - } - } - - Ok(false) - } - - /// Check if the provided key is valid - /// and is authorized to make a specific action. - pub fn authenticate(&self, key: &[u8], action: Action, index: Option<&str>) -> Result { - if self.is_key_authorized(key, action, index)? { - self.is_key_valid(key) - } else { - Ok(false) - } - } } pub struct AuthFilter { @@ -258,12 +246,6 @@ pub struct IndexSearchRules { pub filter: Option, } -fn generate_key(master_key: &[u8], keyid: &str) -> String { - let key = [keyid.as_bytes(), master_key].concat(); - let sha = Sha256::digest(&key); - format!("{}{:x}", keyid, sha) -} - fn generate_default_keys(store: &HeedAuthStore) -> Result<()> { store.put_api_key(Key::default_admin())?; store.put_api_key(Key::default_search())?; diff --git a/meilisearch-auth/src/store.rs b/meilisearch-auth/src/store.rs index 4bd3cdded..69c4cbd57 100644 --- a/meilisearch-auth/src/store.rs +++ b/meilisearch-auth/src/store.rs @@ -1,4 +1,3 @@ -use enum_iterator::IntoEnumIterator; use std::borrow::Cow; use std::cmp::Reverse; use std::convert::TryFrom; @@ -8,20 +7,22 @@ use std::path::Path; use std::str; use std::sync::Arc; +use enum_iterator::IntoEnumIterator; use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; +use sha2::{Digest, Sha256}; use time::OffsetDateTime; +use uuid::Uuid; use super::error::Result; use super::{Action, Key}; const AUTH_STORE_SIZE: usize = 1_073_741_824; //1GiB -pub const KEY_ID_LENGTH: usize = 8; const AUTH_DB_PATH: &str = "auth"; const KEY_DB_NAME: &str = "api-keys"; const KEY_ID_ACTION_INDEX_EXPIRATION_DB_NAME: &str = "keyid-action-index-expiration"; -pub type KeyId = [u8; KEY_ID_LENGTH]; +pub type KeyId = Uuid; #[derive(Clone)] pub struct HeedAuthStore { @@ -73,12 +74,13 @@ impl HeedAuthStore { } pub fn put_api_key(&self, key: Key) -> Result { + let uid = key.uid; let mut wtxn = self.env.write_txn()?; - self.keys.put(&mut wtxn, &key.id, &key)?; - let id = key.id; + self.keys.put(&mut wtxn, uid.as_bytes(), &key)?; + // delete key from inverted database before refilling it. - self.delete_key_from_inverted_db(&mut wtxn, &id)?; + self.delete_key_from_inverted_db(&mut wtxn, &uid)?; // create inverted database. let db = self.action_keyid_index_expiration; @@ -93,13 +95,13 @@ impl HeedAuthStore { for action in actions { if no_index_restriction { // If there is no index restriction we put None. - db.put(&mut wtxn, &(&id, &action, None), &key.expires_at)?; + db.put(&mut wtxn, &(&uid, &action, None), &key.expires_at)?; } else { // else we create a key for each index. for index in key.indexes.iter() { db.put( &mut wtxn, - &(&id, &action, Some(index.as_bytes())), + &(&uid, &action, Some(index.as_bytes())), &key.expires_at, )?; } @@ -111,24 +113,39 @@ impl HeedAuthStore { Ok(key) } - pub fn get_api_key(&self, key: impl AsRef) -> Result> { + pub fn get_api_key(&self, uid: Uuid) -> Result> { let rtxn = self.env.read_txn()?; - match self.get_key_id(key.as_ref().as_bytes()) { - Some(id) => self.keys.get(&rtxn, &id).map_err(|e| e.into()), - None => Ok(None), - } + self.keys.get(&rtxn, uid.as_bytes()).map_err(|e| e.into()) } - pub fn delete_api_key(&self, key: impl AsRef) -> Result { + pub fn get_uid_from_encoded_key( + &self, + encoded_key: &[u8], + master_key: &[u8], + ) -> Result> { + let rtxn = self.env.read_txn()?; + let uid = self + .keys + .remap_data_type::() + .iter(&rtxn)? + .filter_map(|res| match res { + Ok((uid, _)) + if generate_key_as_base64(uid, master_key).as_bytes() == encoded_key => + { + let (uid, _) = try_split_array_at(uid)?; + Some(Uuid::from_bytes(*uid)) + } + _ => None, + }) + .next(); + + Ok(uid) + } + + pub fn delete_api_key(&self, uid: Uuid) -> Result { let mut wtxn = self.env.write_txn()?; - let existing = match self.get_key_id(key.as_ref().as_bytes()) { - Some(id) => { - let existing = self.keys.delete(&mut wtxn, &id)?; - self.delete_key_from_inverted_db(&mut wtxn, &id)?; - existing - } - None => false, - }; + let existing = self.keys.delete(&mut wtxn, uid.as_bytes())?; + self.delete_key_from_inverted_db(&mut wtxn, &uid)?; wtxn.commit()?; Ok(existing) @@ -147,49 +164,37 @@ impl HeedAuthStore { pub fn get_expiration_date( &self, - key: &[u8], + uid: Uuid, action: Action, index: Option<&[u8]>, ) -> Result>> { let rtxn = self.env.read_txn()?; - match self.get_key_id(key) { - Some(id) => { - let tuple = (&id, &action, index); - Ok(self.action_keyid_index_expiration.get(&rtxn, &tuple)?) - } - None => Ok(None), - } + let tuple = (&uid, &action, index); + Ok(self.action_keyid_index_expiration.get(&rtxn, &tuple)?) } pub fn prefix_first_expiration_date( &self, - key: &[u8], + uid: Uuid, action: Action, ) -> Result>> { let rtxn = self.env.read_txn()?; - match self.get_key_id(key) { - Some(id) => { - let tuple = (&id, &action, None); - Ok(self - .action_keyid_index_expiration - .prefix_iter(&rtxn, &tuple)? - .next() - .transpose()? - .map(|(_, expiration)| expiration)) - } - None => Ok(None), - } - } + let tuple = (&uid, &action, None); + let exp = self + .action_keyid_index_expiration + .prefix_iter(&rtxn, &tuple)? + .next() + .transpose()? + .map(|(_, expiration)| expiration); - pub fn get_key_id(&self, key: &[u8]) -> Option { - try_split_array_at::<_, KEY_ID_LENGTH>(key).map(|(id, _)| *id) + Ok(exp) } fn delete_key_from_inverted_db(&self, wtxn: &mut RwTxn, key: &KeyId) -> Result<()> { let mut iter = self .action_keyid_index_expiration .remap_types::() - .prefix_iter_mut(wtxn, key)?; + .prefix_iter_mut(wtxn, key.as_bytes())?; while iter.next().transpose()?.is_some() { // safety: we don't keep references from inside the LMDB database. unsafe { iter.del_current()? }; @@ -207,14 +212,15 @@ impl<'a> milli::heed::BytesDecode<'a> for KeyIdActionCodec { type DItem = (KeyId, Action, Option<&'a [u8]>); fn bytes_decode(bytes: &'a [u8]) -> Option { - let (key_id, action_bytes) = try_split_array_at(bytes)?; + let (key_id_bytes, action_bytes) = try_split_array_at(bytes)?; let (action_bytes, index) = match try_split_array_at(action_bytes)? { (action, []) => (action, None), (action, index) => (action, Some(index)), }; + let key_id = Uuid::from_bytes(*key_id_bytes); let action = Action::from_repr(u8::from_be_bytes(*action_bytes))?; - Some((*key_id, action, index)) + Some((key_id, action, index)) } } @@ -224,7 +230,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec { fn bytes_encode((key_id, action, index): &Self::EItem) -> Option> { let mut bytes = Vec::new(); - bytes.extend_from_slice(*key_id); + bytes.extend_from_slice(key_id.as_bytes()); let action_bytes = u8::to_be_bytes(action.repr()); bytes.extend_from_slice(&action_bytes); if let Some(index) = index { @@ -235,6 +241,12 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec { } } +pub fn generate_key_as_base64(uid: &[u8], master_key: &[u8]) -> String { + let key = [uid, master_key].concat(); + let sha = Sha256::digest(&key); + base64::encode_config(sha, base64::URL_SAFE_NO_PAD) +} + /// Divides one slice into two at an index, returns `None` if mid is out of bounds. pub fn try_split_at(slice: &[T], mid: usize) -> Option<(&[T], &[T])> { if mid <= slice.len() { diff --git a/meilisearch-error/src/lib.rs b/meilisearch-error/src/lib.rs index 11613497c..6e6273db2 100644 --- a/meilisearch-error/src/lib.rs +++ b/meilisearch-error/src/lib.rs @@ -166,6 +166,10 @@ pub enum Code { InvalidApiKeyIndexes, InvalidApiKeyExpiresAt, InvalidApiKeyDescription, + InvalidApiKeyName, + InvalidApiKeyUid, + ImmutableField, + ApiKeyAlreadyExists, } impl Code { @@ -272,6 +276,10 @@ impl Code { InvalidApiKeyDescription => { ErrCode::invalid("invalid_api_key_description", StatusCode::BAD_REQUEST) } + InvalidApiKeyName => ErrCode::invalid("invalid_api_key_name", StatusCode::BAD_REQUEST), + InvalidApiKeyUid => ErrCode::invalid("invalid_api_key_uid", StatusCode::BAD_REQUEST), + ApiKeyAlreadyExists => ErrCode::invalid("api_key_already_exists", StatusCode::CONFLICT), + ImmutableField => ErrCode::invalid("immutable_field", StatusCode::BAD_REQUEST), InvalidMinWordLengthForTypo => { ErrCode::invalid("invalid_min_word_length_for_typo", StatusCode::BAD_REQUEST) } diff --git a/meilisearch-http/Cargo.toml b/meilisearch-http/Cargo.toml index 75d0ac06e..ba11b20e0 100644 --- a/meilisearch-http/Cargo.toml +++ b/meilisearch-http/Cargo.toml @@ -75,7 +75,7 @@ thiserror = "1.0.30" time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } tokio = { version = "1.17.0", features = ["full"] } tokio-stream = "0.1.8" -uuid = { version = "0.8.2", features = ["serde"] } +uuid = { version = "0.8.2", features = ["serde", "v4"] } walkdir = "2.3.2" [dev-dependencies] diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index c4cd9ef14..7732bd7fa 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -132,6 +132,7 @@ pub mod policies { use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; + use uuid::Uuid; use crate::extractors::authentication::Policy; use meilisearch_auth::{Action, AuthController, AuthFilter, SearchRules}; @@ -146,34 +147,21 @@ pub mod policies { validation } - /// Extracts the key prefix used to sign the payload from the payload, without performing any validation. - fn extract_key_prefix(token: &str) -> Option { + /// Extracts the key id used to sign the payload, without performing any validation. + fn extract_key_id(token: &str) -> Option { 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()?; // get token fields without validating it. - let Claims { api_key_prefix, .. } = token_data.claims; - Some(api_key_prefix) + let Claims { api_key_uid, .. } = token_data.claims; + Some(api_key_uid) } - pub struct MasterPolicy; - - impl Policy for MasterPolicy { - fn authenticate( - auth: AuthController, - token: &str, - _index: Option<&str>, - ) -> Option { - if let Some(master_key) = auth.get_master_key() { - if master_key == token { - return Some(AuthFilter::default()); - } - } - - None - } + fn is_keys_action(action: u8) -> bool { + use actions::*; + matches!(action, KEYS_GET | KEYS_CREATE | KEYS_UPDATE | KEYS_DELETE) } pub struct ActionPolicy; @@ -185,7 +173,12 @@ pub mod policies { index: Option<&str>, ) -> Option { // authenticate if token is the master key. - if auth.get_master_key().map_or(true, |mk| mk == token) { + // master key can only have access to keys routes. + // if master key is None only keys routes are inaccessible. + if auth + .get_master_key() + .map_or_else(|| !is_keys_action(A), |mk| mk == token) + { return Some(AuthFilter::default()); } @@ -195,8 +188,10 @@ pub mod policies { return Some(filters); } else if let Some(action) = Action::from_repr(A) { // API key - if let Ok(true) = auth.authenticate(token.as_bytes(), action, index) { - return auth.get_key_filters(token, None).ok(); + if let Ok(Some(uid)) = auth.get_optional_uid_from_encoded_key(token.as_bytes()) { + if let Ok(true) = auth.is_key_authorized(uid, action, index) { + return auth.get_key_filters(uid, None).ok(); + } } } @@ -215,14 +210,11 @@ pub mod policies { return None; } - let api_key_prefix = extract_key_prefix(token)?; + let uid = extract_key_id(token)?; // check if parent key is authorized to do the action. - if auth - .is_key_authorized(api_key_prefix.as_bytes(), Action::Search, index) - .ok()? - { + if auth.is_key_authorized(uid, Action::Search, index).ok()? { // Check if tenant token is valid. - let key = auth.generate_key(&api_key_prefix)?; + let key = auth.generate_key(uid)?; let data = decode::( token, &DecodingKey::from_secret(key.as_bytes()), @@ -245,7 +237,7 @@ pub mod policies { } return auth - .get_key_filters(api_key_prefix, Some(data.claims.search_rules)) + .get_key_filters(uid, Some(data.claims.search_rules)) .ok(); } @@ -258,6 +250,6 @@ pub mod policies { struct Claims { search_rules: SearchRules, exp: Option, - api_key_prefix: String, + api_key_uid: Uuid, } } diff --git a/meilisearch-http/src/routes/api_key.rs b/meilisearch-http/src/routes/api_key.rs index 310b09c4d..831a350d8 100644 --- a/meilisearch-http/src/routes/api_key.rs +++ b/meilisearch-http/src/routes/api_key.rs @@ -1,4 +1,5 @@ use std::str; +use uuid::Uuid; use actix_web::{web, HttpRequest, HttpResponse}; @@ -20,7 +21,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .route(web::get().to(SeqHandler(list_api_keys))), ) .service( - web::resource("/{api_key}") + web::resource("/{key}") .route(web::get().to(SeqHandler(get_api_key))) .route(web::patch().to(SeqHandler(patch_api_key))) .route(web::delete().to(SeqHandler(delete_api_key))), @@ -28,7 +29,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } pub async fn create_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, body: web::Json, _req: HttpRequest, ) -> Result { @@ -44,7 +45,7 @@ pub async fn create_api_key( } pub async fn list_api_keys( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, _req: HttpRequest, ) -> Result { let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { @@ -62,12 +63,16 @@ pub async fn list_api_keys( } pub async fn get_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, path: web::Path, ) -> Result { - let api_key = path.into_inner().api_key; + let key = path.into_inner().key; + let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let key = auth_controller.get_key(&api_key)?; + let uid = + Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?; + let key = auth_controller.get_key(uid)?; + Ok(KeyView::from_key(key, &auth_controller)) }) .await @@ -77,14 +82,17 @@ pub async fn get_api_key( } pub async fn patch_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, body: web::Json, path: web::Path, ) -> Result { - let api_key = path.into_inner().api_key; + let key = path.into_inner().key; let body = body.into_inner(); let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let key = auth_controller.update_key(&api_key, body)?; + let uid = + Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?; + let key = auth_controller.update_key(uid, body)?; + Ok(KeyView::from_key(key, &auth_controller)) }) .await @@ -94,27 +102,33 @@ pub async fn patch_api_key( } pub async fn delete_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, path: web::Path, ) -> Result { - let api_key = path.into_inner().api_key; - tokio::task::spawn_blocking(move || auth_controller.delete_key(&api_key)) - .await - .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??; + let key = path.into_inner().key; + tokio::task::spawn_blocking(move || { + let uid = + Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?; + auth_controller.delete_key(uid) + }) + .await + .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??; Ok(HttpResponse::NoContent().finish()) } #[derive(Deserialize)] pub struct AuthParam { - api_key: String, + key: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct KeyView { + name: Option, description: Option, key: String, + uid: Uuid, actions: Vec, indexes: Vec, #[serde(serialize_with = "time::serde::rfc3339::option::serialize")] @@ -127,12 +141,13 @@ struct KeyView { impl KeyView { fn from_key(key: Key, auth: &AuthController) -> Self { - let key_id = str::from_utf8(&key.id).unwrap(); - let generated_key = auth.generate_key(key_id).unwrap_or_default(); + let generated_key = auth.generate_key(key.uid).unwrap_or_default(); KeyView { + name: key.name, description: key.description, key: generated_key, + uid: key.uid, actions: key.actions, indexes: key.indexes, expires_at: key.expires_at, diff --git a/meilisearch-http/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index e9fb3d127..4eb1fdd6f 100644 --- a/meilisearch-http/tests/auth/api_keys.rs +++ b/meilisearch-http/tests/auth/api_keys.rs @@ -9,7 +9,9 @@ async fn add_valid_api_key() { server.use_api_key("MASTER_KEY"); let content = json!({ + "name": "indexing-key", "description": "Indexing API key", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", "indexes": ["products"], "actions": [ "search", @@ -31,13 +33,16 @@ async fn add_valid_api_key() { }); let (response, code) = server.add_api_key(content).await; + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); let expected_response = json!({ + "name": "indexing-key", "description": "Indexing API key", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", "indexes": ["products"], "actions": [ "search", @@ -59,7 +64,6 @@ async fn add_valid_api_key() { }); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 201); } #[actix_rt::test] @@ -90,7 +94,8 @@ async fn add_valid_api_key_expired_at() { }); let (response, code) = server.add_api_key(content).await; - assert!(response["key"].is_string(), "{:?}", response); + assert_eq!(201, code, "{:?}", &response); + assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); @@ -118,7 +123,6 @@ async fn add_valid_api_key_expired_at() { }); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 201); } #[actix_rt::test] @@ -128,23 +132,19 @@ async fn add_valid_api_key_no_description() { let content = json!({ "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00" }); let (response, code) = server.add_api_key(content).await; - + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); let expected_response = json!({ - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "indexes": [ "products" ], @@ -152,7 +152,6 @@ async fn add_valid_api_key_no_description() { }); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 201); } #[actix_rt::test] @@ -163,23 +162,19 @@ async fn add_valid_api_key_null_description() { let content = json!({ "description": Value::Null, "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00" }); let (response, code) = server.add_api_key(content).await; - + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); let expected_response = json!({ - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "indexes": [ "products" ], @@ -187,7 +182,6 @@ async fn add_valid_api_key_null_description() { }); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 201); } #[actix_rt::test] @@ -196,12 +190,11 @@ async fn error_add_api_key_no_header() { let content = json!({ "description": "Indexing API key", "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(401, code, "{:?}", &response); let expected_response = json!({ "message": "The Authorization header is missing. It must use the bearer authorization method.", @@ -211,7 +204,6 @@ async fn error_add_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -222,12 +214,11 @@ async fn error_add_api_key_bad_key() { let content = json!({ "description": "Indexing API key", "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(403, code, "{:?}", &response); let expected_response = json!({ "message": "The provided API key is invalid.", @@ -237,7 +228,6 @@ async fn error_add_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -252,6 +242,7 @@ async fn error_add_api_key_missing_parameter() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": "`indexes` field is mandatory.", @@ -261,7 +252,6 @@ async fn error_add_api_key_missing_parameter() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); // missing actions let content = json!({ @@ -270,6 +260,7 @@ async fn error_add_api_key_missing_parameter() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": "`actions` field is mandatory.", @@ -279,7 +270,6 @@ async fn error_add_api_key_missing_parameter() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); // missing expiration date let content = json!({ @@ -288,6 +278,7 @@ async fn error_add_api_key_missing_parameter() { "actions": ["documents.add"], }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": "`expiresAt` field is mandatory.", @@ -297,7 +288,6 @@ async fn error_add_api_key_missing_parameter() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -308,12 +298,11 @@ async fn error_add_api_key_invalid_parameters_description() { let content = json!({ "description": {"name":"products"}, "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`description` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#, @@ -323,7 +312,30 @@ async fn error_add_api_key_invalid_parameters_description() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); +} + +#[actix_rt::test] +async fn error_add_api_key_invalid_parameters_name() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + let content = json!({ + "name": {"name":"products"}, + "indexes": ["products"], + "actions": ["documents.add"], + "expiresAt": "2050-11-13T00:00:00Z" + }); + let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); + + let expected_response = json!({ + "message": r#"`name` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#, + "code": "invalid_api_key_name", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" + }); + + assert_eq!(response, expected_response); } #[actix_rt::test] @@ -334,12 +346,11 @@ async fn error_add_api_key_invalid_parameters_indexes() { let content = json!({ "description": "Indexing API key", "indexes": {"name":"products"}, - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`indexes` field value `{"name":"products"}` is invalid. It should be an array of string representing index names."#, @@ -349,7 +360,6 @@ async fn error_add_api_key_invalid_parameters_indexes() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -364,6 +374,7 @@ async fn error_add_api_key_invalid_parameters_actions() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`actions` field value `{"name":"products"}` is invalid. It should be an array of string representing action names."#, @@ -373,7 +384,6 @@ async fn error_add_api_key_invalid_parameters_actions() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); let content = json!({ "description": "Indexing API key", @@ -384,6 +394,7 @@ async fn error_add_api_key_invalid_parameters_actions() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`actions` field value `["doc.add"]` is invalid. It should be an array of string representing action names."#, @@ -393,7 +404,6 @@ async fn error_add_api_key_invalid_parameters_actions() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -404,12 +414,11 @@ async fn error_add_api_key_invalid_parameters_expires_at() { let content = json!({ "description": "Indexing API key", "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": {"name":"products"} }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`expiresAt` field value `{"name":"products"}` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'."#, @@ -419,7 +428,6 @@ async fn error_add_api_key_invalid_parameters_expires_at() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -430,12 +438,11 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() { let content = json!({ "description": "Indexing API key", "indexes": ["products"], - "actions": [ - "documents.add" - ], + "actions": ["documents.add"], "expiresAt": "2010-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": r#"`expiresAt` field value `"2010-11-13T00:00:00Z"` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'."#, @@ -445,7 +452,60 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); +} + +#[actix_rt::test] +async fn error_add_api_key_invalid_parameters_uid() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + let content = json!({ + "description": "Indexing API key", + "uid": "aaaaabbbbbccc", + "indexes": ["products"], + "actions": ["documents.add"], + "expiresAt": "2050-11-13T00:00:00Z" + }); + let (response, code) = server.add_api_key(content).await; + assert_eq!(400, code, "{:?}", &response); + + let expected_response = json!({ + "message": r#"`uid` field value `"aaaaabbbbbccc"` is invalid. It should be a valid UUID v4 string or omitted."#, + "code": "invalid_api_key_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_uid" + }); + + assert_eq!(response, expected_response); +} + +#[actix_rt::test] +async fn error_add_api_key_parameters_uid_already_exist() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + let content = json!({ + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "indexes": ["products"], + "actions": ["search"], + "expiresAt": "2050-11-13T00:00:00Z" + }); + + // first creation is valid. + let (response, code) = server.add_api_key(content.clone()).await; + assert_eq!(201, code, "{:?}", &response); + + // uid already exist. + let (response, code) = server.add_api_key(content).await; + assert_eq!(409, code, "{:?}", &response); + + let expected_response = json!({ + "message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` is already an existing API key.", + "code": "api_key_already_exists", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api_key_already_exists" + }); + + assert_eq!(response, expected_response); } #[actix_rt::test] @@ -453,9 +513,11 @@ async fn get_api_key() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); + let uid = "4bc0887a-0e41-4f3b-935d-0c451dcee9c8"; let content = json!({ "description": "Indexing API key", "indexes": ["products"], + "uid": uid.to_string(), "actions": [ "search", "documents.add", @@ -477,20 +539,15 @@ async fn get_api_key() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); let key = response["key"].as_str().unwrap(); - let (response, code) = server.get_api_key(&key).await; - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - let expected_response = json!({ "description": "Indexing API key", "indexes": ["products"], + "uid": uid.to_string(), "actions": [ "search", "documents.add", @@ -510,8 +567,23 @@ async fn get_api_key() { "expiresAt": "2050-11-13T00:00:00Z" }); - assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 200); + // get with uid + let (response, code) = server.get_api_key(&uid).await; + assert_eq!(200, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["expiresAt"].is_string()); + assert!(response["createdAt"].is_string()); + assert!(response["updatedAt"].is_string()); + assert_json_include!(actual: response, expected: &expected_response); + + // get with key + let (response, code) = server.get_api_key(&key).await; + assert_eq!(200, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["expiresAt"].is_string()); + assert!(response["createdAt"].is_string()); + assert!(response["updatedAt"].is_string()); + assert_json_include!(actual: response, expected: &expected_response); } #[actix_rt::test] @@ -521,6 +593,7 @@ async fn error_get_api_key_no_header() { let (response, code) = server .get_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(401, code, "{:?}", &response); let expected_response = json!({ "message": "The Authorization header is missing. It must use the bearer authorization method.", @@ -530,7 +603,6 @@ async fn error_get_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -541,6 +613,7 @@ async fn error_get_api_key_bad_key() { let (response, code) = server .get_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(403, code, "{:?}", &response); let expected_response = json!({ "message": "The provided API key is invalid.", @@ -550,7 +623,6 @@ async fn error_get_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -561,6 +633,7 @@ async fn error_get_api_key_not_found() { let (response, code) = server .get_api_key("d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(404, code, "{:?}", &response); let expected_response = json!({ "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", @@ -570,7 +643,6 @@ async fn error_get_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -600,11 +672,12 @@ async fn list_api_keys() { "expiresAt": "2050-11-13T00:00:00Z" }); - let (_response, code) = server.add_api_key(content).await; + let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); let (response, code) = server.list_api_keys().await; + assert_eq!(200, code, "{:?}", &response); let expected_response = json!({ "results": [ @@ -630,13 +703,15 @@ async fn list_api_keys() { "expiresAt": "2050-11-13T00:00:00Z" }, { - "description": "Default Search API Key (Use it to search from the frontend)", + "name": "Default Search API Key", + "description": "Use it to search from the frontend", "indexes": ["*"], "actions": ["search"], "expiresAt": serde_json::Value::Null, }, { - "description": "Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)", + "name": "Default Admin API Key", + "description": "Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend", "indexes": ["*"], "actions": ["*"], "expiresAt": serde_json::Value::Null, @@ -644,7 +719,6 @@ async fn list_api_keys() { ]}); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 200); } #[actix_rt::test] @@ -652,6 +726,7 @@ async fn error_list_api_keys_no_header() { let server = Server::new_auth().await; let (response, code) = server.list_api_keys().await; + assert_eq!(401, code, "{:?}", &response); let expected_response = json!({ "message": "The Authorization header is missing. It must use the bearer authorization method.", @@ -661,7 +736,6 @@ async fn error_list_api_keys_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -670,6 +744,7 @@ async fn error_list_api_keys_bad_key() { server.use_api_key("d4000bd7225f77d1eb22cc706ed36772bbc36767c016a27f76def7537b68600d"); let (response, code) = server.list_api_keys().await; + assert_eq!(403, code, "{:?}", &response); let expected_response = json!({ "message": "The provided API key is invalid.", @@ -679,7 +754,6 @@ async fn error_list_api_keys_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -711,17 +785,17 @@ async fn delete_api_key() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); - let key = response["key"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); - let (_response, code) = server.delete_api_key(&key).await; - assert_eq!(code, 204); + let (response, code) = server.delete_api_key(&uid).await; + assert_eq!(204, code, "{:?}", &response); // check if API key no longer exist. - let (_response, code) = server.get_api_key(&key).await; - assert_eq!(code, 404); + let (response, code) = server.get_api_key(&uid).await; + assert_eq!(404, code, "{:?}", &response); } #[actix_rt::test] @@ -731,6 +805,7 @@ async fn error_delete_api_key_no_header() { let (response, code) = server .delete_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(401, code, "{:?}", &response); let expected_response = json!({ "message": "The Authorization header is missing. It must use the bearer authorization method.", @@ -740,7 +815,6 @@ async fn error_delete_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -751,6 +825,7 @@ async fn error_delete_api_key_bad_key() { let (response, code) = server .delete_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(403, code, "{:?}", &response); let expected_response = json!({ "message": "The provided API key is invalid.", @@ -760,7 +835,6 @@ async fn error_delete_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -771,6 +845,7 @@ async fn error_delete_api_key_not_found() { let (response, code) = server .delete_api_key("d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; + assert_eq!(404, code, "{:?}", &response); let expected_response = json!({ "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", @@ -780,7 +855,6 @@ async fn error_delete_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -808,12 +882,12 @@ async fn patch_api_key_description() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); - let key = response["key"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); let created_at = response["createdAt"].as_str().unwrap(); let updated_at = response["updatedAt"].as_str().unwrap(); @@ -821,7 +895,8 @@ async fn patch_api_key_description() { let content = json!({ "description": "Indexing API key" }); thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server.patch_api_key(&key, content).await; + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); @@ -848,18 +923,18 @@ async fn patch_api_key_description() { }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); // Change the description - let content = json!({ "description": "Porduct API key" }); + let content = json!({ "description": "Product API key" }); - let (response, code) = server.patch_api_key(&key, content).await; + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); let expected = json!({ - "description": "Porduct API key", + "description": "Product API key", "indexes": ["products"], "actions": [ "search", @@ -878,12 +953,12 @@ async fn patch_api_key_description() { }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); // Remove the description let content = json!({ "description": serde_json::Value::Null }); - let (response, code) = server.patch_api_key(&key, content).await; + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["expiresAt"].is_string()); assert!(response["createdAt"].is_string()); @@ -907,11 +982,137 @@ async fn patch_api_key_description() { }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] -async fn patch_api_key_indexes() { +async fn patch_api_key_name() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + let content = json!({ + "indexes": ["products"], + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create", + "dumps.get" + ], + "expiresAt": "2050-11-13T00:00:00Z" + }); + + let (response, code) = server.add_api_key(content).await; + // must pass if add_valid_api_key test passes. + assert_eq!(201, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["createdAt"].is_string()); + assert!(response["updatedAt"].is_string()); + + let uid = response["uid"].as_str().unwrap(); + let created_at = response["createdAt"].as_str().unwrap(); + let updated_at = response["updatedAt"].as_str().unwrap(); + + // Add a name + let content = json!({ "name": "Indexing API key" }); + + thread::sleep(time::Duration::new(1, 0)); + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["expiresAt"].is_string()); + assert!(response["createdAt"].is_string()); + assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); + assert_eq!(response["createdAt"].as_str().unwrap(), created_at); + + let expected = json!({ + "name": "Indexing API key", + "indexes": ["products"], + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create", + "dumps.get" + ], + "expiresAt": "2050-11-13T00:00:00Z" + }); + + assert_json_include!(actual: response, expected: expected); + + // Change the name + let content = json!({ "name": "Product API key" }); + + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["expiresAt"].is_string()); + assert!(response["createdAt"].is_string()); + + let expected = json!({ + "name": "Product API key", + "indexes": ["products"], + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create", + "dumps.get" + ], + "expiresAt": "2050-11-13T00:00:00Z" + }); + + assert_json_include!(actual: response, expected: expected); + + // Remove the name + let content = json!({ "name": serde_json::Value::Null }); + + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(200, code, "{:?}", &response); + assert!(response["key"].is_string()); + assert!(response["expiresAt"].is_string()); + assert!(response["createdAt"].is_string()); + + let expected = json!({ + "indexes": ["products"], + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create", + "dumps.get" + ], + "expiresAt": "2050-11-13T00:00:00Z" + }); + + assert_json_include!(actual: response, expected: expected); +} + +#[actix_rt::test] +async fn error_patch_api_key_indexes() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -936,50 +1137,30 @@ async fn patch_api_key_indexes() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); - let key = response["key"].as_str().unwrap(); - let created_at = response["createdAt"].as_str().unwrap(); - let updated_at = response["updatedAt"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); let content = json!({ "indexes": ["products", "prices"] }); thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server.patch_api_key(&key, content).await; - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); - assert_eq!(response["createdAt"].as_str().unwrap(), created_at); + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(400, code, "{:?}", &response); - let expected = json!({ - "description": "Indexing API key", - "indexes": ["products", "prices"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - "dumps.get" - ], - "expiresAt": "2050-11-13T00:00:00Z" + let expected = json!({"message": "`indexes` field cannot be modified for the given resource.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_field" }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] -async fn patch_api_key_actions() { +async fn error_patch_api_key_actions() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1004,14 +1185,12 @@ async fn patch_api_key_actions() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); - let key = response["key"].as_str().unwrap(); - let created_at = response["createdAt"].as_str().unwrap(); - let updated_at = response["updatedAt"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); let content = json!({ "actions": [ @@ -1024,32 +1203,20 @@ async fn patch_api_key_actions() { }); thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server.patch_api_key(&key, content).await; - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); - assert_eq!(response["createdAt"].as_str().unwrap(), created_at); + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(400, code, "{:?}", &response); - let expected = json!({ - "description": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.get", - "indexes.get", - "tasks.get", - "settings.get", - ], - "expiresAt": "2050-11-13T00:00:00Z" + let expected = json!({"message": "`actions` field cannot be modified for the given resource.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_field" }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] -async fn patch_api_key_expiration_date() { +async fn error_patch_api_key_expiration_date() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1074,46 +1241,26 @@ async fn patch_api_key_expiration_date() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); assert!(response["createdAt"].is_string()); assert!(response["updatedAt"].is_string()); - let key = response["key"].as_str().unwrap(); - let created_at = response["createdAt"].as_str().unwrap(); - let updated_at = response["updatedAt"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); let content = json!({ "expiresAt": "2055-11-13T00:00:00Z" }); thread::sleep(time::Duration::new(1, 0)); - let (response, code) = server.patch_api_key(&key, content).await; - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); - assert_eq!(response["createdAt"].as_str().unwrap(), created_at); + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(400, code, "{:?}", &response); - let expected = json!({ - "description": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - "dumps.get" - ], - "expiresAt": "2055-11-13T00:00:00Z" + let expected = json!({"message": "`expiresAt` field cannot be modified for the given resource.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_field" }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] @@ -1126,6 +1273,7 @@ async fn error_patch_api_key_no_header() { json!({}), ) .await; + assert_eq!(401, code, "{:?}", &response); let expected_response = json!({ "message": "The Authorization header is missing. It must use the bearer authorization method.", @@ -1135,7 +1283,6 @@ async fn error_patch_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -1149,6 +1296,7 @@ async fn error_patch_api_key_bad_key() { json!({}), ) .await; + assert_eq!(403, code, "{:?}", &response); let expected_response = json!({ "message": "The provided API key is invalid.", @@ -1158,7 +1306,6 @@ async fn error_patch_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -1172,6 +1319,7 @@ async fn error_patch_api_key_not_found() { json!({}), ) .await; + assert_eq!(404, code, "{:?}", &response); let expected_response = json!({ "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", @@ -1181,7 +1329,6 @@ async fn error_patch_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -1200,17 +1347,18 @@ async fn error_patch_api_key_indexes_invalid_parameters() { let (response, code) = server.add_api_key(content).await; // must pass if add_valid_api_key test passes. - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); - let key = response["key"].as_str().unwrap(); + let uid = response["uid"].as_str().unwrap(); // invalid description let content = json!({ "description": 13 }); - let (response, code) = server.patch_api_key(&key, content).await; + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ "message": "`description` field value `13` is invalid. It should be a string or specified as a null value.", @@ -1220,56 +1368,23 @@ async fn error_patch_api_key_indexes_invalid_parameters() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); - // invalid indexes + // invalid name let content = json!({ - "indexes": 13 + "name": 13 }); - let (response, code) = server.patch_api_key(&key, content).await; + let (response, code) = server.patch_api_key(&uid, content).await; + assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ - "message": "`indexes` field value `13` is invalid. It should be an array of string representing index names.", - "code": "invalid_api_key_indexes", + "message": "`name` field value `13` is invalid. It should be a string or specified as a null value.", + "code": "invalid_api_key_name", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" }); assert_eq!(response, expected_response); - assert_eq!(code, 400); - - // invalid actions - let content = json!({ - "actions": 13 - }); - let (response, code) = server.patch_api_key(&key, content).await; - - let expected_response = json!({ - "message": "`actions` field value `13` is invalid. It should be an array of string representing action names.", - "code": "invalid_api_key_actions", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 400); - - // invalid expiresAt - let content = json!({ - "expiresAt": 13 - }); - let (response, code) = server.patch_api_key(&key, content).await; - - let expected_response = json!({ - "message": "`expiresAt` field value `13` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", - "code": "invalid_api_key_expires_at", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_api_key_expires_at" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -1286,23 +1401,23 @@ async fn error_access_api_key_routes_no_master_key_set() { let (response, code) = server.add_api_key(json!({})).await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.patch_api_key("content", json!({})).await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.get_api_key("content").await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.list_api_keys().await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); server.use_api_key("MASTER_KEY"); @@ -1315,21 +1430,21 @@ async fn error_access_api_key_routes_no_master_key_set() { let (response, code) = server.add_api_key(json!({})).await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.patch_api_key("content", json!({})).await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.get_api_key("content").await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); let (response, code) = server.list_api_keys().await; + assert_eq!(expected_code, code, "{:?}", &response); assert_eq!(response, expected_response); - assert_eq!(code, expected_code); } diff --git a/meilisearch-http/tests/auth/authorization.rs b/meilisearch-http/tests/auth/authorization.rs index fc18758ef..2080e2990 100644 --- a/meilisearch-http/tests/auth/authorization.rs +++ b/meilisearch-http/tests/auth/authorization.rs @@ -46,6 +46,11 @@ pub static AUTHORIZATIONS: Lazy hashset!{"stats.get", "*"}, ("POST", "/dumps") => hashset!{"dumps.create", "*"}, ("GET", "/version") => hashset!{"version", "*"}, + ("PATCH", "/keys/mykey/") => hashset!{"keys.update", "*"}, + ("GET", "/keys/mykey/") => hashset!{"keys.get", "*"}, + ("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"}, + ("POST", "/keys") => hashset!{"keys.create", "*"}, + ("GET", "/keys") => hashset!{"keys.get", "*"}, } }); @@ -80,7 +85,7 @@ async fn error_access_expired_key() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); let key = response["key"].as_str().unwrap(); @@ -92,8 +97,14 @@ async fn error_access_expired_key() { for (method, route) in AUTHORIZATIONS.keys() { let (response, code) = server.dummy_request(method, route).await; - assert_eq!(response, INVALID_RESPONSE.clone()); - assert_eq!(code, 403); + assert_eq!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?}", + method, + route + ); + assert_eq!(403, code, "{:?}", &response); } } @@ -110,7 +121,7 @@ async fn error_access_unauthorized_index() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); let key = response["key"].as_str().unwrap(); @@ -123,8 +134,14 @@ async fn error_access_unauthorized_index() { { let (response, code) = server.dummy_request(method, route).await; - assert_eq!(response, INVALID_RESPONSE.clone()); - assert_eq!(code, 403); + assert_eq!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?}", + method, + route + ); + assert_eq!(403, code, "{:?}", &response); } } @@ -132,36 +149,54 @@ async fn error_access_unauthorized_index() { #[cfg_attr(target_os = "windows", ignore)] async fn error_access_unauthorized_action() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); - - let content = json!({ - "indexes": ["products"], - "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(); - server.use_api_key(&key); for ((method, route), action) in AUTHORIZATIONS.iter() { + // create a new API key letting only the needed action. server.use_api_key("MASTER_KEY"); - // Patch API key letting all rights but the needed one. let content = json!({ + "indexes": ["products"], "actions": ALL_ACTIONS.difference(action).collect::>(), + "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + 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 (response, code) = server.dummy_request(method, route).await; - assert_eq!(response, INVALID_RESPONSE.clone()); - assert_eq!(code, 403); + assert_eq!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?}", + method, + route + ); + assert_eq!(403, code, "{:?}", &response); + } +} + +#[actix_rt::test] +#[cfg_attr(target_os = "windows", ignore)] +async fn access_authorized_master_key() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + // master key must have access to all routes. + for ((method, route), _) in AUTHORIZATIONS.iter() { + let (response, code) = server.dummy_request(method, route).await; + + assert_ne!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?}", + method, + route + ); + assert_ne!(code, 403); } } @@ -169,36 +204,34 @@ async fn error_access_unauthorized_action() { #[cfg_attr(target_os = "windows", ignore)] async fn access_authorized_restricted_index() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); - - let content = json!({ - "indexes": ["products"], - "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(); - server.use_api_key(&key); - for ((method, route), actions) in AUTHORIZATIONS.iter() { for action in actions { - // Patch API key letting only the needed action. + // create a new API key letting only the needed action. + server.use_api_key("MASTER_KEY"); + let content = json!({ + "indexes": ["products"], "actions": [action], + "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); - server.use_api_key("MASTER_KEY"); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + 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 (response, code) = server.dummy_request(method, route).await; - assert_ne!(response, INVALID_RESPONSE.clone()); + assert_ne!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?} with action: {:?}", + method, + route, + action + ); assert_ne!(code, 403); } } @@ -208,36 +241,35 @@ async fn access_authorized_restricted_index() { #[cfg_attr(target_os = "windows", ignore)] async fn access_authorized_no_index_restriction() { 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(); - server.use_api_key(&key); for ((method, route), actions) in AUTHORIZATIONS.iter() { for action in actions { + // create a new API key letting only the needed action. server.use_api_key("MASTER_KEY"); - // Patch API key letting only the needed action. let content = json!({ + "indexes": ["products"], "actions": [action], + "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + 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 (response, code) = server.dummy_request(method, route).await; - assert_ne!(response, INVALID_RESPONSE.clone()); + assert_ne!( + response, + INVALID_RESPONSE.clone(), + "on route: {:?} - {:?} with action: {:?}", + method, + route, + action + ); assert_ne!(code, 403); } } @@ -247,16 +279,16 @@ async fn access_authorized_no_index_restriction() { #[cfg_attr(target_os = "windows", ignore)] async fn access_authorized_stats_restricted_index() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on `products` index only. @@ -266,7 +298,7 @@ async fn access_authorized_stats_restricted_index() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -274,7 +306,7 @@ async fn access_authorized_stats_restricted_index() { server.use_api_key(&key); let (response, code) = server.stats().await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); // key should have access on `products` index. assert!(response["indexes"].get("products").is_some()); @@ -287,16 +319,16 @@ async fn access_authorized_stats_restricted_index() { #[cfg_attr(target_os = "windows", ignore)] async fn access_authorized_stats_no_index_restriction() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on all indexes. @@ -306,7 +338,7 @@ async fn access_authorized_stats_no_index_restriction() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -314,7 +346,7 @@ async fn access_authorized_stats_no_index_restriction() { server.use_api_key(&key); let (response, code) = server.stats().await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); // key should have access on `products` index. assert!(response["indexes"].get("products").is_some()); @@ -327,16 +359,16 @@ async fn access_authorized_stats_no_index_restriction() { #[cfg_attr(target_os = "windows", ignore)] async fn list_authorized_indexes_restricted_index() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on `products` index only. @@ -346,7 +378,7 @@ async fn list_authorized_indexes_restricted_index() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -354,7 +386,7 @@ async fn list_authorized_indexes_restricted_index() { server.use_api_key(&key); let (response, code) = server.list_indexes(None, None).await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); let response = response["results"].as_array().unwrap(); // key should have access on `products` index. @@ -368,16 +400,16 @@ async fn list_authorized_indexes_restricted_index() { #[cfg_attr(target_os = "windows", ignore)] async fn list_authorized_indexes_no_index_restriction() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on all indexes. @@ -387,7 +419,7 @@ async fn list_authorized_indexes_no_index_restriction() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -395,7 +427,7 @@ async fn list_authorized_indexes_no_index_restriction() { server.use_api_key(&key); let (response, code) = server.list_indexes(None, None).await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); let response = response["results"].as_array().unwrap(); // key should have access on `products` index. @@ -408,16 +440,16 @@ async fn list_authorized_indexes_no_index_restriction() { #[actix_rt::test] async fn list_authorized_tasks_restricted_index() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on `products` index only. @@ -427,7 +459,7 @@ async fn list_authorized_tasks_restricted_index() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -435,7 +467,7 @@ async fn list_authorized_tasks_restricted_index() { server.use_api_key(&key); let (response, code) = server.service.get("/tasks").await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); println!("{}", response); let response = response["results"].as_array().unwrap(); // key should have access on `products` index. @@ -448,16 +480,16 @@ async fn list_authorized_tasks_restricted_index() { #[actix_rt::test] async fn list_authorized_tasks_no_index_restriction() { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; // create index `test` let index = server.index("test"); - let (_, code) = index.create(Some("id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("id")).await; + assert_eq!(202, code, "{:?}", &response); // create index `products` let index = server.index("products"); - let (_, code) = index.create(Some("product_id")).await; - assert_eq!(code, 202); + let (response, code) = index.create(Some("product_id")).await; + assert_eq!(202, code, "{:?}", &response); index.wait_task(0).await; // create key with access on all indexes. @@ -467,7 +499,7 @@ async fn list_authorized_tasks_no_index_restriction() { "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -475,7 +507,7 @@ async fn list_authorized_tasks_no_index_restriction() { server.use_api_key(&key); let (response, code) = server.service.get("/tasks").await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); let response = response["results"].as_array().unwrap(); // key should have access on `products` index. @@ -498,7 +530,7 @@ async fn error_creating_index_without_action() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -522,7 +554,7 @@ async fn error_creating_index_without_action() { ]); let (response, code) = index.add_documents(documents, None).await; - assert_eq!(code, 202, "{:?}", response); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); let response = index.wait_task(task_id).await; @@ -533,7 +565,7 @@ async fn error_creating_index_without_action() { let settings = json!({ "distinctAttribute": "test"}); let (response, code) = index.update_settings(settings).await; - assert_eq!(code, 202); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); let response = index.wait_task(task_id).await; @@ -543,7 +575,7 @@ async fn error_creating_index_without_action() { // try to create a index via add specialized settings route let (response, code) = index.update_distinct_attribute(json!("test")).await; - assert_eq!(code, 202); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); let response = index.wait_task(task_id).await; @@ -565,7 +597,7 @@ async fn lazy_create_index() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(code, 201); + assert_eq!(201, code, "{:?}", &response); assert!(response["key"].is_string()); // use created key. @@ -582,13 +614,13 @@ async fn lazy_create_index() { ]); let (response, code) = index.add_documents(documents, None).await; - assert_eq!(code, 202, "{:?}", response); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); index.wait_task(task_id).await; let (response, code) = index.get_task(task_id).await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); assert_eq!(response["status"], "succeeded"); // try to create a index via add settings route @@ -596,24 +628,24 @@ async fn lazy_create_index() { let settings = json!({ "distinctAttribute": "test"}); let (response, code) = index.update_settings(settings).await; - assert_eq!(code, 202); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); index.wait_task(task_id).await; let (response, code) = index.get_task(task_id).await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); assert_eq!(response["status"], "succeeded"); // try to create a index via add specialized settings route let index = server.index("test2"); let (response, code) = index.update_distinct_attribute(json!("test")).await; - assert_eq!(code, 202); + assert_eq!(202, code, "{:?}", &response); let task_id = response["taskUid"].as_u64().unwrap(); index.wait_task(task_id).await; let (response, code) = index.get_task(task_id).await; - assert_eq!(code, 200); + assert_eq!(200, code, "{:?}", &response); assert_eq!(response["status"], "succeeded"); } diff --git a/meilisearch-http/tests/auth/mod.rs b/meilisearch-http/tests/auth/mod.rs index ef47f4a6a..03c24dd6d 100644 --- a/meilisearch-http/tests/auth/mod.rs +++ b/meilisearch-http/tests/auth/mod.rs @@ -13,6 +13,15 @@ impl Server { self.service.api_key = Some(api_key.as_ref().to_string()); } + /// Fetch and use the default admin key for nexts http requests. + pub async fn use_admin_key(&mut self, master_key: impl AsRef) { + self.use_api_key(master_key); + let (response, code) = self.list_api_keys().await; + assert_eq!(200, code, "{:?}", response); + let admin_key = &response["results"][1]["key"]; + self.use_api_key(admin_key.as_str().unwrap()); + } + pub async fn add_api_key(&self, content: Value) -> (Value, StatusCode) { let url = "/keys"; self.service.post(url, content).await diff --git a/meilisearch-http/tests/auth/tenant_token.rs b/meilisearch-http/tests/auth/tenant_token.rs index bb9224590..d82e170aa 100644 --- a/meilisearch-http/tests/auth/tenant_token.rs +++ b/meilisearch-http/tests/auth/tenant_token.rs @@ -8,11 +8,15 @@ use time::{Duration, OffsetDateTime}; use super::authorization::{ALL_ACTIONS, AUTHORIZATIONS}; -fn generate_tenant_token(parent_key: impl AsRef, mut body: HashMap<&str, Value>) -> String { +fn generate_tenant_token( + parent_uid: impl AsRef, + parent_key: impl AsRef, + mut body: HashMap<&str, Value>, +) -> String { use jsonwebtoken::{encode, EncodingKey, Header}; - let key_id = &parent_key.as_ref()[..8]; - body.insert("apiKeyPrefix", json!(key_id)); + let parent_uid = parent_uid.as_ref(); + body.insert("apiKeyUid", json!(parent_uid)); encode( &Header::default(), &body, @@ -114,7 +118,7 @@ static REFUSED_KEYS: Lazy> = Lazy::new(|| { macro_rules! compute_autorized_search { ($tenant_tokens:expr, $filter:expr, $expected_count:expr) => { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; @@ -130,9 +134,10 @@ macro_rules! compute_autorized_search { 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(&key, tenant_token.clone()); + let web_token = generate_tenant_token(&uid, &key, tenant_token.clone()); server.use_api_key(&web_token); let index = server.index("sales"); index @@ -160,7 +165,7 @@ macro_rules! compute_autorized_search { macro_rules! compute_forbidden_search { ($tenant_tokens:expr, $parent_keys:expr) => { let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); + server.use_admin_key("MASTER_KEY").await; let index = server.index("sales"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; @@ -172,9 +177,10 @@ macro_rules! compute_forbidden_search { 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(); for tenant_token in $tenant_tokens.iter() { - let web_token = generate_tenant_token(&key, tenant_token.clone()); + let web_token = generate_tenant_token(&uid, &key, tenant_token.clone()); server.use_api_key(&web_token); let index = server.index("sales"); index @@ -461,12 +467,13 @@ async fn error_access_forbidden_routes() { 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(&key, tenant_token); + let web_token = generate_tenant_token(&uid, &key, tenant_token); server.use_api_key(&web_token); for ((method, route), actions) in AUTHORIZATIONS.iter() { @@ -496,12 +503,13 @@ async fn error_access_expired_parent_key() { 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(&key, tenant_token); + 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 @@ -538,12 +546,13 @@ async fn error_access_modified_token() { 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(&key, tenant_token); + let web_token = generate_tenant_token(&uid, &key, tenant_token); server.use_api_key(&web_token); // test search request while web_token is valid @@ -558,7 +567,7 @@ async fn error_access_modified_token() { "exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) }; - let alt = generate_tenant_token(&key, tenant_token); + let alt = generate_tenant_token(&uid, &key, tenant_token); let altered_token = [ web_token.split('.').next().unwrap(), alt.split('.').nth(1).unwrap(), diff --git a/meilisearch-http/tests/common/index.rs b/meilisearch-http/tests/common/index.rs index e21dbcb67..4be8ad873 100644 --- a/meilisearch-http/tests/common/index.rs +++ b/meilisearch-http/tests/common/index.rs @@ -110,7 +110,7 @@ impl Index<'_> { let url = format!("/tasks/{}", update_id); for _ in 0..10 { let (response, status_code) = self.service.get(&url).await; - assert_eq!(status_code, 200, "response: {}", response); + assert_eq!(200, status_code, "response: {}", response); if response["status"] == "succeeded" || response["status"] == "failed" { return response; diff --git a/meilisearch-lib/Cargo.toml b/meilisearch-lib/Cargo.toml index 85ae49f64..bb8e628c1 100644 --- a/meilisearch-lib/Cargo.toml +++ b/meilisearch-lib/Cargo.toml @@ -52,7 +52,7 @@ tempfile = "3.3.0" thiserror = "1.0.30" time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } tokio = { version = "1.17.0", features = ["full"] } -uuid = { version = "0.8.2", features = ["serde"] } +uuid = { version = "0.8.2", features = ["serde", "v4"] } walkdir = "2.3.2" whoami = { version = "1.2.1", optional = true } diff --git a/meilisearch-lib/src/dump/loaders/v4.rs b/meilisearch-lib/src/dump/loaders/v4.rs index 126300af8..0744df7ea 100644 --- a/meilisearch-lib/src/dump/loaders/v4.rs +++ b/meilisearch-lib/src/dump/loaders/v4.rs @@ -1,10 +1,12 @@ use std::fs::{self, create_dir_all, File}; -use std::io::Write; +use std::io::{BufReader, Write}; use std::path::Path; use fs_extra::dir::{self, CopyOptions}; use log::info; +use serde_json::{Deserializer, Map, Value}; use tempfile::tempdir; +use uuid::Uuid; use crate::dump::{compat, Metadata}; use crate::options::IndexerOpts; @@ -24,14 +26,10 @@ pub fn load_dump( let options = CopyOptions::default(); // Indexes - dir::copy(src.as_ref().join("indexes"), patched_dir.path(), &options)?; + dir::copy(src.as_ref().join("indexes"), &patched_dir, &options)?; // Index uuids - dir::copy( - src.as_ref().join("index_uuids"), - patched_dir.path(), - &options, - )?; + dir::copy(src.as_ref().join("index_uuids"), &patched_dir, &options)?; // Metadata fs::copy( @@ -43,13 +41,11 @@ pub fn load_dump( patch_updates(&src, &patched_dir)?; // Keys - if src.as_ref().join("keys").exists() { - fs::copy(src.as_ref().join("keys"), patched_dir.path().join("keys"))?; - } + patch_keys(&src, &patched_dir)?; super::v5::load_dump( meta, - patched_dir.path(), + &patched_dir, dst, index_db_size, meta_env_size, @@ -79,3 +75,29 @@ fn patch_updates(src: impl AsRef, dst: impl AsRef) -> anyhow::Result Ok(()) } + +fn patch_keys(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { + let keys_file_src = src.as_ref().join("keys"); + + if !keys_file_src.exists() { + return Ok(()); + } + + fs::create_dir_all(&dst)?; + let keys_file_dst = dst.as_ref().join("keys"); + let mut writer = File::create(&keys_file_dst)?; + + let reader = BufReader::new(File::open(&keys_file_src)?); + for key in Deserializer::from_reader(reader).into_iter() { + let mut key: Map = key?; + + // generate a new uuid v4 and insert it in the key. + let uid = serde_json::to_value(Uuid::new_v4()).unwrap(); + key.insert("uid".to_string(), uid); + + serde_json::to_writer(&mut writer, &key)?; + writer.write_all(b"\n")?; + } + + Ok(()) +}