Skip to content

Commit 9b10232

Browse files
bors[bot]meili-botbrunoocasali
authored
Merge #231
231: Changes related to the next Meilisearch release (v0.26.0) r=brunoocasali a=meili-bot Related to this issue: meilisearch/integration-guides#181 This PR: - gathers the changes related to the next Meilisearch release (v0.26.0) so that this package is ready when the official release is out. - should pass the tests against the [latest pre-release of Meilisearch](https://github.com/meilisearch/meilisearch/releases). - might eventually contain test failures until the Meilisearch v0.26.0 is out. ⚠️ This PR should NOT be merged until the next release of Meilisearch (v0.26.0) is out. _This PR is auto-generated for the [pre-release week](https://github.com/meilisearch/integration-guides/blob/master/guides/pre-release-week.md) purpose._ Co-authored-by: meili-bot <[email protected]> Co-authored-by: Bruno Casali <[email protected]>
2 parents 4452f2c + 3245d2f commit 9b10232

File tree

10 files changed

+219
-4
lines changed

10 files changed

+219
-4
lines changed

.code-samples.meilisearch.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,17 @@ landing_getting_started_1: |-
792792
Movie { "id": "5".to_string(), "title": "Moana".to_string() },
793793
Movie { "id": "6".to_string(), "title": "Philadelphia".to_string() }
794794
], Some("reference_number")).await.unwrap();
795+
tenant_token_guide_generate_sdk_1: |-
796+
let api_key = "B5KdX2MY2jV6EXfUs6scSfmC...";
797+
let expires_at = time::macros::datetime!(2025 - 12 - 20 00:00:00 UTC);
798+
let search_rules = json!({ "patient_medical_records": { "filter": "user_id = 1" } });
799+
800+
let token = client.generate_tenant_token(search_rules, api_key, expires_at).unwrap();
801+
tenant_token_guide_search_sdk_1: |-
802+
let front_end_client = Client::new("http://127.0.0.1:7700", token);
803+
let results: SearchResults<Patient> = front_end_client.index("patient_medical_records")
804+
.search()
805+
.with_query("blood test")
806+
.execute()
807+
.await
808+
.unwrap();

.github/workflows/pre-release-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ jobs:
2222
- name: Get the latest Meilisearch RC
2323
run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV
2424
- name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker
25-
run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} ./meilisearch --master-key=masterKey --no-analytics=true
25+
run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} ./meilisearch --master-key=masterKey --no-analytics
2626
- name: Run tests
2727
run: cargo test --verbose -- --test-threads=1

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ log = "0.4"
1515
serde = { version = "1.0", features = ["derive"] }
1616
serde_json = "1.0"
1717
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] }
18+
jsonwebtoken = { version = "8", default-features = false }
1819

1920
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
2021
futures = "0.3"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extensi
242242

243243
## 🤖 Compatibility with Meilisearch
244244

245-
This package only guarantees the compatibility with the [version v0.25.0 of MeiliSearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.25.0).
245+
This package only guarantees the compatibility with the [version v0.26.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.26.0).
246246

247247
## ⚙️ Development Workflow and Contributing
248248

README.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extensi
9797

9898
## 🤖 Compatibility with Meilisearch
9999

100-
This package only guarantees the compatibility with the [version v0.25.0 of MeiliSearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.25.0).
100+
This package only guarantees the compatibility with the [version v0.26.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.26.0).
101101

102102
## ⚙️ Development Workflow and Contributing
103103

src/client.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,24 @@ impl Client {
576576

577577
Ok(tasks.results)
578578
}
579+
580+
/// Generates a new tenant token.
581+
///
582+
/// # Example
583+
///
584+
/// ```
585+
/// # use meilisearch_sdk::*;
586+
/// # futures::executor::block_on(async move {
587+
/// # let client = client::Client::new("http://localhost:7700", "masterKey");
588+
/// let token = client.generate_tenant_token(serde_json::json!(["*"]), None, None).unwrap();
589+
/// let client = client::Client::new("http://localhost:7700", token);
590+
/// # });
591+
/// ```
592+
pub fn generate_tenant_token(&self, search_rules: serde_json::Value, api_key: Option<&str>, expires_at: Option<OffsetDateTime>) -> Result<String, Error> {
593+
let api_key = api_key.unwrap_or(&self.api_key);
594+
595+
crate::tenant_tokens::generate_tenant_token(search_rules, api_key, expires_at)
596+
}
579597
}
580598

581599
#[derive(Deserialize)]

src/errors.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ pub enum Error {
1919
/// It probably comes from an invalid API key resulting in an invalid HTTP header.
2020
InvalidRequest,
2121

22+
/// It is not possible to generate a tenant token with a invalid api key.
23+
/// Empty strings or with less than 8 characters are considered invalid.
24+
TenantTokensInvalidApiKey,
25+
/// It is not possible to generate an already expired tenant token.
26+
TenantTokensExpiredSignature,
27+
28+
/// When jsonwebtoken cannot generate the token successfully.
29+
InvalidTenantToken(jsonwebtoken::errors::Error),
30+
2231
/// The http client encountered an error.
2332
#[cfg(not(target_arch = "wasm32"))]
2433
HttpError(isahc::Error),
@@ -52,6 +61,12 @@ impl From<MeilisearchError> for Error {
5261
}
5362
}
5463

64+
impl From<jsonwebtoken::errors::Error> for Error {
65+
fn from(error: jsonwebtoken::errors::Error) -> Error {
66+
Error::InvalidTenantToken(error)
67+
}
68+
}
69+
5570
/// The type of error that was encountered.
5671
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5772
#[serde(rename_all = "snake_case")]
@@ -168,6 +183,9 @@ impl std::fmt::Display for Error {
168183
Error::ParseError(e) => write!(fmt, "Error parsing response JSON: {}", e),
169184
Error::HttpError(e) => write!(fmt, "HTTP request failed: {}", e),
170185
Error::Timeout => write!(fmt, "A task did not succeed in time."),
186+
Error::TenantTokensInvalidApiKey => write!(fmt, "The provided api_key is invalid."),
187+
Error::TenantTokensExpiredSignature => write!(fmt, "The provided expires_at is already expired."),
188+
Error::InvalidTenantToken(e) => write!(fmt, "Impossible to generate the token, jsonwebtoken encountered an error: {}", e)
171189
}
172190
}
173191
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ pub mod search;
230230
pub mod settings;
231231
/// Module representing the [tasks::Task]s.
232232
pub mod tasks;
233+
/// Module that generates tenant tokens.
234+
mod tenant_tokens;
233235

234236
#[cfg(feature = "sync")]
235237
pub(crate) type Rc<T> = std::sync::Arc<T>;

src/search.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ mod tests {
283283
use crate::{client::*, search::*};
284284
use meilisearch_test_macro::meilisearch_test;
285285
use serde::{Deserialize, Serialize};
286-
use serde_json::{Map, Value};
286+
use serde_json::{Map, Value, json};
287287

288288
#[derive(Debug, Serialize, Deserialize, PartialEq)]
289289
struct Document {
@@ -571,4 +571,39 @@ mod tests {
571571
assert_eq!(results.hits.len(), 1);
572572
Ok(())
573573
}
574+
575+
#[meilisearch_test]
576+
async fn test_generate_tenant_token_from_client(client: Client, index: Index) -> Result<(), Error> {
577+
use crate::key::{KeyBuilder, Action};
578+
579+
setup_test_index(&client, &index).await?;
580+
581+
let key = KeyBuilder::new("key for generate_tenant_token test")
582+
.with_action(Action::All)
583+
.with_index("*")
584+
.create(&client).await.unwrap();
585+
let allowed_client = Client::new("http://localhost:7700", key.key);
586+
587+
let search_rules = vec![
588+
json!({ "*": {}}),
589+
json!({ "*": Value::Null }),
590+
json!(["*"]),
591+
json!({ "*": { "filter": "kind = text" } }),
592+
json!([index.uid.to_string()]),
593+
];
594+
595+
for rules in search_rules {
596+
let token = allowed_client.generate_tenant_token(rules, None, None).expect("Cannot generate tenant token.");
597+
let new_client = Client::new("http://localhost:7700", token);
598+
599+
let result: SearchResults<Document> = new_client.index(index.uid.to_string())
600+
.search()
601+
.execute()
602+
.await?;
603+
604+
assert!(!result.hits.is_empty());
605+
}
606+
607+
Ok(())
608+
}
574609
}

src/tenant_tokens.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use crate::{
2+
errors::*
3+
};
4+
use serde::{Serialize, Deserialize};
5+
use jsonwebtoken::{encode, Header, EncodingKey};
6+
use time::{OffsetDateTime};
7+
use serde_json::Value;
8+
9+
#[derive(Debug, Serialize, Deserialize)]
10+
#[serde(rename_all = "camelCase")]
11+
struct TenantTokenClaim {
12+
api_key_prefix: String,
13+
search_rules: Value,
14+
#[serde(with = "time::serde::timestamp::option")]
15+
exp: Option<OffsetDateTime>,
16+
}
17+
18+
pub fn generate_tenant_token(search_rules: Value, api_key: impl AsRef<str>, expires_at: Option<OffsetDateTime>) -> Result<String, Error> {
19+
if api_key.as_ref().chars().count() < 8 {
20+
return Err(Error::TenantTokensInvalidApiKey)
21+
}
22+
23+
if expires_at.map_or(false, |expires_at| OffsetDateTime::now_utc() > expires_at) {
24+
return Err(Error::TenantTokensExpiredSignature)
25+
}
26+
27+
let key_prefix = api_key.as_ref().chars().take(8).collect();
28+
let claims = TenantTokenClaim {
29+
api_key_prefix: key_prefix,
30+
exp: expires_at,
31+
search_rules
32+
};
33+
34+
let token = encode(
35+
&Header::default(),
36+
&claims,
37+
&EncodingKey::from_secret(api_key.as_ref().as_bytes()),
38+
);
39+
40+
Ok(token?)
41+
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
use serde_json::json;
46+
use crate::tenant_tokens::*;
47+
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
48+
use std::collections::HashSet;
49+
50+
const SEARCH_RULES: [&str; 1] = ["*"];
51+
const VALID_KEY: &str = "a19b6ec84ee31324efa560cd1f7e6939";
52+
53+
fn build_validation() -> Validation {
54+
let mut validation = Validation::new(Algorithm::HS256);
55+
validation.validate_exp = false;
56+
validation.required_spec_claims = HashSet::new();
57+
58+
validation
59+
}
60+
61+
#[test]
62+
fn test_generate_token_with_given_key() {
63+
let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap();
64+
65+
let valid_key = decode::<TenantTokenClaim>(
66+
&token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &build_validation()
67+
);
68+
let invalid_key = decode::<TenantTokenClaim>(
69+
&token, &DecodingKey::from_secret("not-the-same-key".as_ref()), &build_validation()
70+
);
71+
72+
assert!(valid_key.is_ok());
73+
assert!(invalid_key.is_err());
74+
}
75+
76+
#[test]
77+
fn test_generate_token_without_key() {
78+
let key = String::from("");
79+
let token = generate_tenant_token(json!(SEARCH_RULES), &key, None);
80+
81+
assert!(token.is_err());
82+
}
83+
84+
#[test]
85+
fn test_generate_token_with_expiration() {
86+
let exp = OffsetDateTime::now_utc() + time::Duration::HOUR;
87+
let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap();
88+
89+
let decoded = decode::<TenantTokenClaim>(
90+
&token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &Validation::new(Algorithm::HS256)
91+
);
92+
93+
assert!(decoded.is_ok());
94+
}
95+
96+
#[test]
97+
fn test_generate_token_with_expires_at_in_the_past() {
98+
let exp = OffsetDateTime::now_utc() - time::Duration::HOUR;
99+
let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp));
100+
101+
assert!(token.is_err());
102+
}
103+
104+
#[test]
105+
fn test_generate_token_contains_claims() {
106+
let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap();
107+
108+
let decoded = decode::<TenantTokenClaim>(
109+
&token, &DecodingKey::from_secret(VALID_KEY.as_ref()), &build_validation()
110+
).expect("Cannot decode the token");
111+
112+
assert_eq!(decoded.claims.api_key_prefix, &VALID_KEY[..8]);
113+
assert_eq!(decoded.claims.search_rules, json!(SEARCH_RULES));
114+
}
115+
116+
#[test]
117+
fn test_generate_token_with_multi_byte_chars() {
118+
let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09";
119+
let token = generate_tenant_token(json!(SEARCH_RULES), key, None).unwrap();
120+
121+
let decoded = decode::<TenantTokenClaim>(
122+
&token, &DecodingKey::from_secret(key.as_ref()), &build_validation()
123+
).expect("Cannot decode the token");
124+
125+
assert_eq!(decoded.claims.api_key_prefix, "Ëa1ทt9bV");
126+
}
127+
}

0 commit comments

Comments
 (0)