diff --git a/Cargo.toml b/Cargo.toml index 078f256d..9724a196 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde_json = "1.0" time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] } jsonwebtoken = { version = "8", default-features = false } yaup = "0.2.0" +uuid = { version = "1.1.2", features = ["v4"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures = "0.3" diff --git a/src/client.rs b/src/client.rs index a7bcc1a9..15a605e4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -806,19 +806,21 @@ impl Client { /// # /// # futures::executor::block_on(async move { /// # let client = client::Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let token = client.generate_tenant_token(serde_json::json!(["*"]), None, None).unwrap(); + /// let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + /// let token = client.generate_tenant_token(api_key_uid, serde_json::json!(["*"]), None, None).unwrap(); /// let client = client::Client::new(MEILISEARCH_HOST, token); /// # }); /// ``` pub fn generate_tenant_token( &self, + api_key_uid: String, search_rules: serde_json::Value, api_key: Option<&str>, expires_at: Option, ) -> Result { let api_key = api_key.unwrap_or(&self.api_key); - crate::tenant_tokens::generate_tenant_token(search_rules, api_key, expires_at) + crate::tenant_tokens::generate_tenant_token(api_key_uid, search_rules, api_key, expires_at) } } diff --git a/src/errors.rs b/src/errors.rs index 68f78650..b1daf14e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -36,6 +36,10 @@ pub enum Error { HttpError(String), // The library formating the query parameters encountered an error. Yaup(yaup::Error), + // The library validating the format of an uuid. + Uuid(uuid::Error), + // Error thrown in case the version of the Uuid is not v4. + InvalidUuid4Version, } #[derive(Debug, Clone, Deserialize)] @@ -75,6 +79,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: uuid::Error) -> Error { + Error::Uuid(error) + } +} + /// The type of error that was encountered. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -194,7 +204,9 @@ impl std::fmt::Display for Error { Error::TenantTokensInvalidApiKey => write!(fmt, "The provided api_key is invalid."), Error::TenantTokensExpiredSignature => write!(fmt, "The provided expires_at is already expired."), Error::InvalidTenantToken(e) => write!(fmt, "Impossible to generate the token, jsonwebtoken encountered an error: {}", e), - Error::Yaup(e) => write!(fmt, "Internal Error: could not parse the query parameters: {}", e) + Error::Yaup(e) => write!(fmt, "Internal Error: could not parse the query parameters: {}", e), + Error::Uuid(e) => write!(fmt, "The uid of the token has bit an uuid4 format: {}", e), + Error::InvalidUuid4Version => write!(fmt, "The uid provided to the token is not of version uuidv4") } } } diff --git a/src/indexes.rs b/src/indexes.rs index 106da0e3..1309e8dd 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -370,10 +370,8 @@ impl Index { request::<(), DocumentsResults>(&url, &self.client.api_key, Method::Get(()), 200).await } + /// Get [Document]s by batch with parameters. - /// - /// # Example - /// /// ``` /// use serde::{Serialize, Deserialize}; /// @@ -409,7 +407,6 @@ impl Index { pub async fn get_documents_with( &self, documents_query: &DocumentsQuery<'_>, - ) -> Result, Error> { let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); request::<&DocumentsQuery, DocumentsResults>( &url, diff --git a/src/search.rs b/src/search.rs index c1abaa07..67776cee 100644 --- a/src/search.rs +++ b/src/search.rs @@ -666,7 +666,6 @@ mod tests { query.with_highlight_post_tag(" ⊂(´• ω •`⊂)"); let results: SearchResults = index.execute_query(&query).await?; - dbg!(&results); assert_eq!( &Document { id: 2, @@ -784,9 +783,10 @@ mod tests { for rules in search_rules { let token = allowed_client - .generate_tenant_token(rules, None, None) + .generate_tenant_token(key.uid.clone(), rules, None, None) .expect("Cannot generate tenant token."); - let new_client = Client::new(meilisearch_host, token); + + let new_client = Client::new(meilisearch_host, token.clone()); let result: SearchResults = new_client .index(index.uid.to_string()) diff --git a/src/tenant_tokens.rs b/src/tenant_tokens.rs index 369f401b..83fc3704 100644 --- a/src/tenant_tokens.rs +++ b/src/tenant_tokens.rs @@ -1,34 +1,41 @@ -use crate::{ - errors::* -}; -use serde::{Serialize, Deserialize}; -use jsonwebtoken::{encode, Header, EncodingKey}; -use time::{OffsetDateTime}; +use crate::errors::*; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase")] struct TenantTokenClaim { - api_key_prefix: String, + api_key_uid: String, search_rules: Value, #[serde(with = "time::serde::timestamp::option")] exp: Option, } -pub fn generate_tenant_token(search_rules: Value, api_key: impl AsRef, expires_at: Option) -> Result { - if api_key.as_ref().chars().count() < 8 { - return Err(Error::TenantTokensInvalidApiKey) +pub fn generate_tenant_token( + api_key_uid: String, + search_rules: Value, + api_key: impl AsRef, + expires_at: Option, +) -> Result { + // Validate uuid format + let uid = Uuid::try_parse(&api_key_uid)?; + + // Validate uuid version + if uid.get_version_num() != 4 { + return Err(Error::InvalidUuid4Version); } if expires_at.map_or(false, |expires_at| OffsetDateTime::now_utc() > expires_at) { - return Err(Error::TenantTokensExpiredSignature) + return Err(Error::TenantTokensExpiredSignature); } - let key_prefix = api_key.as_ref().chars().take(8).collect(); let claims = TenantTokenClaim { - api_key_prefix: key_prefix, + api_key_uid, exp: expires_at, - search_rules + search_rules, }; let token = encode( @@ -42,9 +49,9 @@ pub fn generate_tenant_token(search_rules: Value, api_key: impl AsRef, expi #[cfg(test)] mod tests { - use serde_json::json; use crate::tenant_tokens::*; - use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; + use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; + use serde_json::json; use std::collections::HashSet; const SEARCH_RULES: [&str; 1] = ["*"]; @@ -60,13 +67,19 @@ mod tests { #[test] fn test_generate_token_with_given_key() { - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap(); + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + let token = + generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, None).unwrap(); let valid_key = decode::( - &token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &build_validation() + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &build_validation(), ); let invalid_key = decode::( - &token, &DecodingKey::from_secret("not-the-same-key".as_ref()), &build_validation() + &token, + &DecodingKey::from_secret("not-the-same-key".as_ref()), + &build_validation(), ); assert!(valid_key.is_ok()); @@ -74,20 +87,25 @@ mod tests { } #[test] - fn test_generate_token_without_key() { + fn test_generate_token_without_uid() { + let api_key_uid = "".to_string(); let key = String::from(""); - let token = generate_tenant_token(json!(SEARCH_RULES), &key, None); + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), &key, None); assert!(token.is_err()); } #[test] fn test_generate_token_with_expiration() { + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let exp = OffsetDateTime::now_utc() + time::Duration::HOUR; - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap(); + let token = + generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap(); let decoded = decode::( - &token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &Validation::new(Algorithm::HS256) + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &Validation::new(Algorithm::HS256), ); assert!(decoded.is_ok()); @@ -95,33 +113,63 @@ mod tests { #[test] fn test_generate_token_with_expires_at_in_the_past() { + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let exp = OffsetDateTime::now_utc() - time::Duration::HOUR; - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp)); + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)); assert!(token.is_err()); } #[test] fn test_generate_token_contains_claims() { - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap(); + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + let token = + generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), VALID_KEY, None) + .unwrap(); let decoded = decode::( - &token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &build_validation() - ).expect("Cannot decode the token"); + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &build_validation(), + ) + .expect("Cannot decode the token"); - assert_eq!(decoded.claims.api_key_prefix, &VALID_KEY[..8]); + assert_eq!(decoded.claims.api_key_uid, api_key_uid); assert_eq!(decoded.claims.search_rules, json!(SEARCH_RULES)); } #[test] fn test_generate_token_with_multi_byte_chars() { + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; - let token = generate_tenant_token(json!(SEARCH_RULES), key, None).unwrap(); + let token = + generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None).unwrap(); let decoded = decode::( - &token, &DecodingKey::from_secret(key.as_ref()), &build_validation() - ).expect("Cannot decode the token"); + &token, + &DecodingKey::from_secret(key.as_ref()), + &build_validation(), + ) + .expect("Cannot decode the token"); + + assert_eq!(decoded.claims.api_key_uid, api_key_uid); + } + + #[test] + fn test_generate_token_with_wrongly_formated_uid() { + let api_key_uid = "xxx".to_string(); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None); - assert_eq!(decoded.claims.api_key_prefix, "Ëa1ทt9bV"); + assert!(token.is_err()); + } + + #[test] + fn test_generate_token_with_wrong_uid_version() { + let api_key_uid = "6a11eb96-2485-11ed-861d-0242ac120002".to_string(); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None); + + assert!(token.is_err()); } }