From e2c204cf86e6b71d9ccb9c512558707acfb94bc4 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Mon, 23 May 2022 17:03:28 +0200 Subject: [PATCH 01/18] Update tests to fit to the new requirements --- meilisearch-http/tests/auth/api_keys.rs | 513 +++++++++++++------ meilisearch-http/tests/auth/authorization.rs | 272 ++++++---- meilisearch-http/tests/auth/mod.rs | 9 + meilisearch-http/tests/auth/tenant_token.rs | 31 +- meilisearch-http/tests/common/index.rs | 2 +- 5 files changed, 556 insertions(+), 271 deletions(-) diff --git a/meilisearch-http/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index e9fb3d127..7919c8ee9 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,31 @@ async fn error_add_api_key_invalid_parameters_indexes() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); +} + +#[ignore] +#[actix_rt::test] +async fn error_add_api_key_invalid_index_uid_format() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + let content = json!({ + "description": "Indexing API key", + "indexes": ["inv@lid uid"], + "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": "`inv@lid uid` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" + }); + + assert_eq!(response, expected_response); } #[actix_rt::test] @@ -364,6 +399,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 +409,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 +419,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 +429,6 @@ async fn error_add_api_key_invalid_parameters_actions() { }); assert_eq!(response, expected_response); - assert_eq!(code, 400); } #[actix_rt::test] @@ -404,12 +439,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 +453,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 +463,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 +477,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 uuidv4 string or ommited."#, + "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` already exists for an 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 +538,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 +564,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 +592,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 +618,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 +628,6 @@ async fn error_get_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -541,6 +638,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 +648,6 @@ async fn error_get_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -561,6 +658,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 +668,6 @@ async fn error_get_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -600,11 +697,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": [ @@ -644,7 +742,6 @@ async fn list_api_keys() { ]}); assert_json_include!(actual: response, expected: expected_response); - assert_eq!(code, 200); } #[actix_rt::test] @@ -652,6 +749,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 +759,6 @@ async fn error_list_api_keys_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -670,6 +767,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 +777,6 @@ async fn error_list_api_keys_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -711,17 +808,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 +828,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 +838,6 @@ async fn error_delete_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -751,6 +848,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 +858,6 @@ async fn error_delete_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -771,6 +868,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 +878,6 @@ async fn error_delete_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -808,12 +905,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 +918,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 +946,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 +976,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 +1005,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 patch_api_key_indexes_unchanged() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -936,19 +1160,20 @@ 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 uid = response["uid"].as_str().unwrap(); let created_at = response["createdAt"].as_str().unwrap(); let updated_at = response["updatedAt"].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; + 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()); @@ -957,7 +1182,7 @@ async fn patch_api_key_indexes() { let expected = json!({ "description": "Indexing API key", - "indexes": ["products", "prices"], + "indexes": ["products"], "actions": [ "search", "documents.add", @@ -975,11 +1200,10 @@ async fn patch_api_key_indexes() { }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] -async fn patch_api_key_actions() { +async fn patch_api_key_actions_unchanged() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1004,12 +1228,13 @@ 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 uid = response["uid"].as_str().unwrap(); + let created_at = response["createdAt"].as_str().unwrap(); let updated_at = response["updatedAt"].as_str().unwrap(); @@ -1024,7 +1249,8 @@ async fn patch_api_key_actions() { }); 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()); @@ -1036,20 +1262,25 @@ async fn patch_api_key_actions() { "indexes": ["products"], "actions": [ "search", + "documents.add", "documents.get", + "documents.delete", + "indexes.create", "indexes.get", - "tasks.get", - "settings.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create", + "dumps.get" ], "expiresAt": "2050-11-13T00:00:00Z" }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] -async fn patch_api_key_expiration_date() { +async fn patch_api_key_expiration_date_unchanged() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1074,19 +1305,20 @@ 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 uid = response["uid"].as_str().unwrap(); let created_at = response["createdAt"].as_str().unwrap(); let updated_at = response["updatedAt"].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; + 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()); @@ -1109,11 +1341,10 @@ async fn patch_api_key_expiration_date() { "dumps.create", "dumps.get" ], - "expiresAt": "2055-11-13T00:00:00Z" + "expiresAt": "2050-11-13T00:00:00Z" }); assert_json_include!(actual: response, expected: expected); - assert_eq!(code, 200); } #[actix_rt::test] @@ -1126,6 +1357,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 +1367,6 @@ async fn error_patch_api_key_no_header() { }); assert_eq!(response, expected_response); - assert_eq!(code, 401); } #[actix_rt::test] @@ -1149,6 +1380,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 +1390,6 @@ async fn error_patch_api_key_bad_key() { }); assert_eq!(response, expected_response); - assert_eq!(code, 403); } #[actix_rt::test] @@ -1172,6 +1403,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 +1413,6 @@ async fn error_patch_api_key_not_found() { }); assert_eq!(response, expected_response); - assert_eq!(code, 404); } #[actix_rt::test] @@ -1200,17 +1431,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 +1452,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 +1485,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 +1514,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..81c626215 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); } } @@ -141,7 +158,7 @@ async fn error_access_unauthorized_action() { }); 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(); @@ -154,14 +171,68 @@ async fn error_access_unauthorized_action() { let content = json!({ "actions": ALL_ACTIONS.difference(action).collect::>(), }); - let (_, code) = server.patch_api_key(&key, content).await; - assert_eq!(code, 200); + let (response, code) = server.patch_api_key(&key, content).await; + assert_eq!(200, code, "{:?}", &response); 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 error_access_master_key() { + let mut server = Server::new_auth().await; + server.use_api_key("MASTER_KEY"); + + // master key must only have access to /keys + for ((method, route), _) in AUTHORIZATIONS + .iter() + .filter(|(_, action)| action.iter().all(|a| !a.starts_with("keys."))) + { + let (response, code) = server.dummy_request(method, route).await; + + 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 only have access to /keys + for ((method, route), _) in AUTHORIZATIONS + .iter() + .filter(|(_, action)| action.iter().any(|a| a.starts_with("keys."))) + { + 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 +240,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 +277,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 +315,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 +334,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 +342,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 +355,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 +374,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 +382,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 +395,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 +414,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 +422,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 +436,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 +455,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 +463,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 +476,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 +495,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 +503,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 +516,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 +535,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 +543,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 +566,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 +590,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 +601,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 +611,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 +633,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 +650,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 +664,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; From 96a5791e395829c8770f3519fee3ba0e3b988d20 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 25 May 2022 10:32:47 +0200 Subject: [PATCH 02/18] Add uid and name fields in keys --- Cargo.lock | 1 + meilisearch-auth/Cargo.toml | 1 + meilisearch-auth/src/error.rs | 11 ++ meilisearch-auth/src/key.rs | 50 +++--- meilisearch-auth/src/lib.rs | 143 ++++++++---------- meilisearch-auth/src/store.rs | 106 +++++++------ meilisearch-error/src/lib.rs | 6 + .../src/extractors/authentication/mod.rs | 28 ++-- meilisearch-http/src/routes/api_key.rs | 36 +++-- 9 files changed, 205 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39eb78987..f48e6c59d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1982,6 +1982,7 @@ dependencies = [ "sha2", "thiserror", "time 0.3.9", + "uuid", ] [[package]] diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index dd12b5b63..9a7ce0d3e 100644 --- a/meilisearch-auth/Cargo.toml +++ b/meilisearch-auth/Cargo.toml @@ -13,3 +13,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"] } diff --git a/meilisearch-auth/src/error.rs b/meilisearch-auth/src/error.rs index 8a87eda27..dc6301348 100644 --- a/meilisearch-auth/src/error.rs +++ b/meilisearch-auth/src/error.rs @@ -18,8 +18,16 @@ 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 uuidv4 string or ommited.")] + InvalidApiKeyUid(Value), #[error("API key `{0}` not found.")] ApiKeyNotFound(String), + #[error("`uid` field value `{0}` already exists for an API key.")] + ApiKeyAlreadyExists(String), #[error("Internal error: {0}")] Internal(Box), } @@ -39,7 +47,10 @@ 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::Internal(_) => Code::Internal, } } diff --git a/meilisearch-auth/src/key.rs b/meilisearch-auth/src/key.rs index 1b06f34be..d69c0aed4 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,6 +28,15 @@ pub struct Key { impl Key { pub fn create_from_value(value: Value) -> Result { + let name = match value.get("name") { + Some(Value::Null) => None, + Some(des) => Some( + from_value(des.clone()) + .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?, + ), + None => None, + }; + let description = match value.get("description") { Some(Value::Null) => None, Some(des) => Some( @@ -34,7 +46,13 @@ impl Key { None => None, }; - let id = generate_id(); + 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 +79,9 @@ impl Key { let updated_at = created_at; Ok(Self { + name, description, - id, + uid, actions, indexes, expires_at, @@ -101,9 +120,11 @@ impl Key { pub(crate) fn default_admin() -> Self { let now = OffsetDateTime::now_utc(); + let uid = Uuid::new_v4(); Self { + name: Some("admin".to_string()), 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(), + uid, actions: vec![Action::All], indexes: vec!["*".to_string()], expires_at: None, @@ -114,11 +135,13 @@ impl Key { pub(crate) fn default_search() -> Self { let now = OffsetDateTime::now_utc(); + let uid = Uuid::new_v4(); Self { + name: Some("search".to_string()), description: Some( "Default Search API Key (Use it to search from the frontend)".to_string(), ), - id: generate_id(), + uid, actions: vec![Action::Search], indexes: vec!["*".to_string()], expires_at: None, @@ -128,19 +151,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..9f9c59c35 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -4,14 +4,15 @@ pub mod error; mod key; mod store; +use crate::store::generate_key; use std::collections::{HashMap, HashSet}; use std::path::Path; -use std::str::from_utf8; + use std::sync::Arc; +use uuid::Uuid; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha2::{Digest, Sha256}; use time::OffsetDateTime; pub use action::{actions, Action}; @@ -42,62 +43,73 @@ 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_uid_from_sha(&self, key: &[u8]) -> Result> { + match &self.master_key { + Some(master_key) => self.store.get_uid_from_sha(key, master_key.as_bytes()), + None => Ok(None), + } + } + + pub fn try_get_uid_from_sha(&self, key: &str) -> Result { + self.get_uid_from_sha(key.as_bytes())? + .ok_or_else(|| AuthControllerError::ApiKeyNotFound(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 +117,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 +131,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(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 +166,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 +245,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..762e707bc 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,33 @@ 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_sha(&self, key_sha: &[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(uid, master_key).as_bytes() == key_sha => { + 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 +158,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 +206,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 +224,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 +235,12 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec { } } +pub fn generate_key(uid: &[u8], master_key: &[u8]) -> String { + let key = [uid, master_key].concat(); + let sha = Sha256::digest(&key); + format!("{:x}", sha) +} + /// 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..57882f8e0 100644 --- a/meilisearch-error/src/lib.rs +++ b/meilisearch-error/src/lib.rs @@ -166,6 +166,9 @@ pub enum Code { InvalidApiKeyIndexes, InvalidApiKeyExpiresAt, InvalidApiKeyDescription, + InvalidApiKeyName, + InvalidApiKeyUid, + ApiKeyAlreadyExists, } impl Code { @@ -272,6 +275,9 @@ 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), InvalidMinWordLengthForTypo => { ErrCode::invalid("invalid_min_word_length_for_typo", StatusCode::BAD_REQUEST) } diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index c4cd9ef14..cf93d363a 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,16 +147,16 @@ 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 from 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 { uid, .. } = token_data.claims; + Some(uid) } pub struct MasterPolicy; @@ -195,8 +196,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_uid_from_sha(token.as_bytes()) { + if let Ok(true) = auth.is_key_authorized(uid, action, index) { + return auth.get_key_filters(uid, None).ok(); + } } } @@ -215,14 +218,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 +245,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 +258,6 @@ pub mod policies { struct Claims { search_rules: SearchRules, exp: Option, - api_key_prefix: String, + uid: Uuid, } } diff --git a/meilisearch-http/src/routes/api_key.rs b/meilisearch-http/src/routes/api_key.rs index 310b09c4d..ba964e5d1 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))), @@ -65,9 +66,12 @@ pub async fn get_api_key( auth_controller: GuardedData, 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.try_get_uid_from_sha(&key))?; + let key = auth_controller.get_key(uid)?; + Ok(KeyView::from_key(key, &auth_controller)) }) .await @@ -81,10 +85,12 @@ pub async fn patch_api_key( 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.try_get_uid_from_sha(&key))?; + let key = auth_controller.update_key(uid, body)?; + Ok(KeyView::from_key(key, &auth_controller)) }) .await @@ -97,24 +103,29 @@ pub async fn delete_api_key( auth_controller: GuardedData, 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.try_get_uid_from_sha(&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 +138,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, From d54643455c81044f9964aea0cff9c2bb327f6262 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 25 May 2022 10:41:06 +0200 Subject: [PATCH 03/18] Make PATCH only modify name, description, and updated_at fields --- meilisearch-auth/src/key.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/meilisearch-auth/src/key.rs b/meilisearch-auth/src/key.rs index d69c0aed4..bdabb2b21 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -97,20 +97,10 @@ 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(ind) = value.get("indexes") { - let ind = from_value(ind.clone()) - .map_err(|_| AuthControllerError::InvalidApiKeyIndexes(ind.clone())); - self.indexes = ind?; - } - - if let Some(exp) = value.get("expiresAt") { - self.expires_at = parse_expiration_date(exp)?; + if let Some(des) = value.get("name") { + let des = from_value(des.clone()) + .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone())); + self.name = des?; } self.updated_at = OffsetDateTime::now_utc(); From 34c8888f5683374d9e021402bbb4d3f3e8f52cc5 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 25 May 2022 15:25:57 +0200 Subject: [PATCH 04/18] Add keys actions --- meilisearch-auth/src/action.rs | 27 +++++++++++++++++++++++--- meilisearch-http/src/routes/api_key.rs | 10 +++++----- 2 files changed, 29 insertions(+), 8 deletions(-) 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-http/src/routes/api_key.rs b/meilisearch-http/src/routes/api_key.rs index ba964e5d1..37ff80ec6 100644 --- a/meilisearch-http/src/routes/api_key.rs +++ b/meilisearch-http/src/routes/api_key.rs @@ -29,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 { @@ -45,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> { @@ -63,7 +63,7 @@ pub async fn list_api_keys( } pub async fn get_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, path: web::Path, ) -> Result { let key = path.into_inner().key; @@ -81,7 +81,7 @@ 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 { @@ -100,7 +100,7 @@ pub async fn patch_api_key( } pub async fn delete_api_key( - auth_controller: GuardedData, + auth_controller: GuardedData, AuthController>, path: web::Path, ) -> Result { let key = path.into_inner().key; From a57b2d95386be28012b026c59647693df8db431f Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 25 May 2022 16:35:00 +0200 Subject: [PATCH 05/18] Restrict master key access to /keys routes --- .../src/extractors/authentication/mod.rs | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index cf93d363a..a0e914ec9 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -159,22 +159,9 @@ pub mod policies { Some(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; @@ -186,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 && is_keys_action(A)) + { return Some(AuthFilter::default()); } From b9a79eb8583807769f8b9a9e27b0001f5f6d43c0 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Mon, 30 May 2022 10:59:48 +0200 Subject: [PATCH 06/18] Change apiKeyPrefix to apiKeyUid --- meilisearch-http/src/extractors/authentication/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index a0e914ec9..fdce0f5f0 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -155,8 +155,8 @@ pub mod policies { let token_data = decode::(token, &dummy_key, &validation).ok()?; // get token fields without validating it. - let Claims { uid, .. } = token_data.claims; - Some(uid) + let Claims { api_key_uid, .. } = token_data.claims; + Some(api_key_uid) } fn is_keys_action(action: u8) -> bool { @@ -250,6 +250,6 @@ pub mod policies { struct Claims { search_rules: SearchRules, exp: Option, - uid: Uuid, + api_key_uid: Uuid, } } From 70916d65968ff7697aabbfc0edf00e26ef38f308 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Mon, 30 May 2022 14:57:10 +0200 Subject: [PATCH 07/18] Patch dump v4 --- meilisearch-auth/src/dump.rs | 26 ++++++++++++++++++++++++++ meilisearch-lib/src/dump/loaders/v4.rs | 6 +++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/meilisearch-auth/src/dump.rs b/meilisearch-auth/src/dump.rs index 77a4aa5ca..1d3bc2139 100644 --- a/meilisearch-auth/src/dump.rs +++ b/meilisearch-auth/src/dump.rs @@ -1,8 +1,11 @@ +use serde_json::{Map, Value}; +use std::fs; use std::fs::File; use std::io::BufRead; use std::io::BufReader; use std::io::Write; use std::path::Path; +use uuid::Uuid; use crate::{AuthController, HeedAuthStore, Result}; @@ -44,4 +47,27 @@ impl AuthController { Ok(()) } + + pub fn patch_dump_v4(src: impl AsRef, dst: impl AsRef) -> Result<()> { + let keys_file_src = src.as_ref().join(KEYS_PATH); + + if !keys_file_src.exists() { + return Ok(()); + } + + fs::create_dir_all(&dst)?; + let keys_file_dst = dst.as_ref().join(KEYS_PATH); + let mut writer = File::create(&keys_file_dst)?; + + let mut reader = BufReader::new(File::open(&keys_file_src)?).lines(); + while let Some(key) = reader.next().transpose()? { + let mut key: Map = serde_json::from_str(&key)?; + let uid = Uuid::new_v4().to_string(); + key.insert("uid".to_string(), Value::String(uid)); + serde_json::to_writer(&mut writer, &key)?; + writer.write_all(b"\n")?; + } + + Ok(()) + } } diff --git a/meilisearch-lib/src/dump/loaders/v4.rs b/meilisearch-lib/src/dump/loaders/v4.rs index 126300af8..3caa7a9e9 100644 --- a/meilisearch-lib/src/dump/loaders/v4.rs +++ b/meilisearch-lib/src/dump/loaders/v4.rs @@ -6,6 +6,8 @@ use fs_extra::dir::{self, CopyOptions}; use log::info; use tempfile::tempdir; +use meilisearch_auth::AuthController; + use crate::dump::{compat, Metadata}; use crate::options::IndexerOpts; use crate::tasks::task::Task; @@ -43,9 +45,7 @@ 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"))?; - } + AuthController::patch_dump_v4(&src, patched_dir.path())?; super::v5::load_dump( meta, From 84f52ac17533fa1eb651d19380abb16ae1fac284 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 13:56:42 +0200 Subject: [PATCH 08/18] Add v4 feature to uuid --- meilisearch-auth/Cargo.toml | 2 +- meilisearch-http/Cargo.toml | 2 +- meilisearch-lib/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index 9a7ce0d3e..7ffa072e8 100644 --- a/meilisearch-auth/Cargo.toml +++ b/meilisearch-auth/Cargo.toml @@ -13,4 +13,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"] } +uuid = { version = "0.8.2", features = ["serde", "v4"] } 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-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 } From 96152a3d32a4c1c076daaada771b8812043281f9 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 14:18:34 +0200 Subject: [PATCH 09/18] Change default API keys names and descriptions --- meilisearch-auth/src/key.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/meilisearch-auth/src/key.rs b/meilisearch-auth/src/key.rs index bdabb2b21..f55993607 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -112,8 +112,8 @@ impl Key { let now = OffsetDateTime::now_utc(); let uid = Uuid::new_v4(); Self { - name: Some("admin".to_string()), - description: Some("Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)".to_string()), + name: Some("Default Admin API Key".to_string()), + description: Some("Use it for all other than search operations. Caution! Do not expose it on a public frontend".to_string()), uid, actions: vec![Action::All], indexes: vec!["*".to_string()], @@ -127,10 +127,8 @@ impl Key { let now = OffsetDateTime::now_utc(); let uid = Uuid::new_v4(); Self { - name: Some("search".to_string()), - description: Some( - "Default Search API Key (Use it to search from the frontend)".to_string(), - ), + 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()], From 151f4941102f6675f15257cebf5d85c528f46e32 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 15:04:33 +0200 Subject: [PATCH 10/18] Use Stream Deserializer to load dumps --- meilisearch-auth/src/dump.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/meilisearch-auth/src/dump.rs b/meilisearch-auth/src/dump.rs index 1d3bc2139..127e65280 100644 --- a/meilisearch-auth/src/dump.rs +++ b/meilisearch-auth/src/dump.rs @@ -1,7 +1,7 @@ +use serde_json::Deserializer; use serde_json::{Map, Value}; use std::fs; use std::fs::File; -use std::io::BufRead; use std::io::BufReader; use std::io::Write; use std::path::Path; @@ -39,10 +39,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(()) @@ -59,11 +58,14 @@ impl AuthController { let keys_file_dst = dst.as_ref().join(KEYS_PATH); let mut writer = File::create(&keys_file_dst)?; - let mut reader = BufReader::new(File::open(&keys_file_src)?).lines(); - while let Some(key) = reader.next().transpose()? { - let mut key: Map = serde_json::from_str(&key)?; - let uid = Uuid::new_v4().to_string(); - key.insert("uid".to_string(), Value::String(uid)); + 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")?; } From b3c8915702e758f6adb1615b3b858c506fd1c6ff Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 15:23:17 +0200 Subject: [PATCH 11/18] Make small changes and renaming --- meilisearch-auth/src/key.rs | 20 ++++++++----------- meilisearch-auth/src/lib.rs | 10 +++++----- .../src/extractors/authentication/mod.rs | 4 ++-- meilisearch-http/src/routes/api_key.rs | 6 +++--- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/meilisearch-auth/src/key.rs b/meilisearch-auth/src/key.rs index f55993607..baac68637 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -29,21 +29,17 @@ pub struct Key { impl Key { pub fn create_from_value(value: Value) -> Result { let name = match value.get("name") { - Some(Value::Null) => None, - Some(des) => Some( - from_value(des.clone()) - .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?, - ), - None => None, + None | Some(Value::Null) => None, + Some(des) => from_value(des.clone()) + .map(Some) + .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?, }; 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, + 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( diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 9f9c59c35..578093abf 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -63,16 +63,16 @@ impl AuthController { .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string())) } - pub fn get_uid_from_sha(&self, key: &[u8]) -> Result> { + pub fn get_optional_uid_from_sha(&self, sha: &[u8]) -> Result> { match &self.master_key { - Some(master_key) => self.store.get_uid_from_sha(key, master_key.as_bytes()), + Some(master_key) => self.store.get_uid_from_sha(sha, master_key.as_bytes()), None => Ok(None), } } - pub fn try_get_uid_from_sha(&self, key: &str) -> Result { - self.get_uid_from_sha(key.as_bytes())? - .ok_or_else(|| AuthControllerError::ApiKeyNotFound(key.to_string())) + pub fn get_uid_from_sha(&self, sha: &str) -> Result { + self.get_optional_uid_from_sha(sha.as_bytes())? + .ok_or_else(|| AuthControllerError::ApiKeyNotFound(sha.to_string())) } pub fn get_key_filters( diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index fdce0f5f0..a6384492c 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -147,7 +147,7 @@ pub mod policies { validation } - /// Extracts the key id used to sign the payload from the payload, without performing any validation. + /// 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(); @@ -188,7 +188,7 @@ pub mod policies { return Some(filters); } else if let Some(action) = Action::from_repr(A) { // API key - if let Ok(Some(uid)) = auth.get_uid_from_sha(token.as_bytes()) { + if let Ok(Some(uid)) = auth.get_optional_uid_from_sha(token.as_bytes()) { if let Ok(true) = auth.is_key_authorized(uid, action, index) { return auth.get_key_filters(uid, None).ok(); } diff --git a/meilisearch-http/src/routes/api_key.rs b/meilisearch-http/src/routes/api_key.rs index 37ff80ec6..cfe81b301 100644 --- a/meilisearch-http/src/routes/api_key.rs +++ b/meilisearch-http/src/routes/api_key.rs @@ -69,7 +69,7 @@ pub async fn get_api_key( let key = path.into_inner().key; let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.try_get_uid_from_sha(&key))?; + let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&key))?; let key = auth_controller.get_key(uid)?; Ok(KeyView::from_key(key, &auth_controller)) @@ -88,7 +88,7 @@ pub async fn patch_api_key( let key = path.into_inner().key; let body = body.into_inner(); let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.try_get_uid_from_sha(&key))?; + let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&key))?; let key = auth_controller.update_key(uid, body)?; Ok(KeyView::from_key(key, &auth_controller)) @@ -105,7 +105,7 @@ pub async fn delete_api_key( ) -> Result { let key = path.into_inner().key; tokio::task::spawn_blocking(move || { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.try_get_uid_from_sha(&key))?; + let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&key))?; auth_controller.delete_key(uid) }) .await From 1f62e832672964b0c72c8fdd3ab787998b07c3bc Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 15:24:26 +0200 Subject: [PATCH 12/18] Remove error_add_api_key_invalid_index_uid_format --- meilisearch-http/tests/auth/api_keys.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/meilisearch-http/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index 7919c8ee9..8e76cdad8 100644 --- a/meilisearch-http/tests/auth/api_keys.rs +++ b/meilisearch-http/tests/auth/api_keys.rs @@ -362,31 +362,6 @@ async fn error_add_api_key_invalid_parameters_indexes() { assert_eq!(response, expected_response); } -#[ignore] -#[actix_rt::test] -async fn error_add_api_key_invalid_index_uid_format() { - let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); - - let content = json!({ - "description": "Indexing API key", - "indexes": ["inv@lid uid"], - "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": "`inv@lid uid` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", - "code": "invalid_api_key_indexes", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" - }); - - assert_eq!(response, expected_response); -} - #[actix_rt::test] async fn error_add_api_key_invalid_parameters_actions() { let mut server = Server::new_auth().await; From c295924ea2d0832975a73e879e4fd43043680233 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 15:31:16 +0200 Subject: [PATCH 13/18] Patch tests --- meilisearch-http/tests/auth/api_keys.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meilisearch-http/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index 8e76cdad8..2d6f07d86 100644 --- a/meilisearch-http/tests/auth/api_keys.rs +++ b/meilisearch-http/tests/auth/api_keys.rs @@ -703,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 all other than search operations. Caution! Do not expose it on a public frontend", "indexes": ["*"], "actions": ["*"], "expiresAt": serde_json::Value::Null, @@ -792,7 +794,7 @@ async fn delete_api_key() { assert_eq!(204, code, "{:?}", &response); // check if API key no longer exist. - let (_response, code) = server.get_api_key(&uid).await; + let (response, code) = server.get_api_key(&uid).await; assert_eq!(404, code, "{:?}", &response); } From 1816db8c1f9328af92336ed05e25f1b1b78ab07d Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Tue, 31 May 2022 15:39:09 +0200 Subject: [PATCH 14/18] Move dump v4 patcher into v4.rs --- meilisearch-auth/src/dump.rs | 30 +----------------- meilisearch-lib/src/dump/loaders/v4.rs | 44 +++++++++++++++++++------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/meilisearch-auth/src/dump.rs b/meilisearch-auth/src/dump.rs index 127e65280..7e607e574 100644 --- a/meilisearch-auth/src/dump.rs +++ b/meilisearch-auth/src/dump.rs @@ -1,11 +1,9 @@ use serde_json::Deserializer; -use serde_json::{Map, Value}; -use std::fs; + use std::fs::File; use std::io::BufReader; use std::io::Write; use std::path::Path; -use uuid::Uuid; use crate::{AuthController, HeedAuthStore, Result}; @@ -46,30 +44,4 @@ impl AuthController { Ok(()) } - - pub fn patch_dump_v4(src: impl AsRef, dst: impl AsRef) -> Result<()> { - let keys_file_src = src.as_ref().join(KEYS_PATH); - - if !keys_file_src.exists() { - return Ok(()); - } - - fs::create_dir_all(&dst)?; - let keys_file_dst = dst.as_ref().join(KEYS_PATH); - 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(()) - } } diff --git a/meilisearch-lib/src/dump/loaders/v4.rs b/meilisearch-lib/src/dump/loaders/v4.rs index 3caa7a9e9..0744df7ea 100644 --- a/meilisearch-lib/src/dump/loaders/v4.rs +++ b/meilisearch-lib/src/dump/loaders/v4.rs @@ -1,12 +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 meilisearch_auth::AuthController; +use uuid::Uuid; use crate::dump::{compat, Metadata}; use crate::options::IndexerOpts; @@ -26,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( @@ -45,11 +41,11 @@ pub fn load_dump( patch_updates(&src, &patched_dir)?; // Keys - AuthController::patch_dump_v4(&src, patched_dir.path())?; + 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(()) +} From b2e2dc855896ee17051bca918df961346b4310b1 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 1 Jun 2022 11:47:44 +0200 Subject: [PATCH 15/18] Re-authorize master_key to access to all routes --- .../src/extractors/authentication/mod.rs | 2 +- meilisearch-http/tests/auth/authorization.rs | 31 ++----------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index a6384492c..99f972984 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -177,7 +177,7 @@ pub mod policies { // 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 && is_keys_action(A)) + .map_or_else(|| !is_keys_action(A), |mk| mk == token) { return Some(AuthFilter::default()); } diff --git a/meilisearch-http/tests/auth/authorization.rs b/meilisearch-http/tests/auth/authorization.rs index 81c626215..fde4c61f3 100644 --- a/meilisearch-http/tests/auth/authorization.rs +++ b/meilisearch-http/tests/auth/authorization.rs @@ -188,41 +188,14 @@ async fn error_access_unauthorized_action() { } } -#[actix_rt::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn error_access_master_key() { - let mut server = Server::new_auth().await; - server.use_api_key("MASTER_KEY"); - - // master key must only have access to /keys - for ((method, route), _) in AUTHORIZATIONS - .iter() - .filter(|(_, action)| action.iter().all(|a| !a.starts_with("keys."))) - { - let (response, code) = server.dummy_request(method, route).await; - - 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 only have access to /keys - for ((method, route), _) in AUTHORIZATIONS - .iter() - .filter(|(_, action)| action.iter().any(|a| a.starts_with("keys."))) - { + // 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!( From 94b32cce01b579ceb2bb22d12a87ee511b0bbcc0 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 1 Jun 2022 14:11:56 +0200 Subject: [PATCH 16/18] Patch errors --- meilisearch-auth/src/error.rs | 7 +- meilisearch-auth/src/key.rs | 24 +++++ meilisearch-error/src/lib.rs | 2 + meilisearch-http/tests/auth/api_keys.rs | 101 ++++--------------- meilisearch-http/tests/auth/authorization.rs | 25 ++--- 5 files changed, 59 insertions(+), 100 deletions(-) diff --git a/meilisearch-auth/src/error.rs b/meilisearch-auth/src/error.rs index dc6301348..dbf28b421 100644 --- a/meilisearch-auth/src/error.rs +++ b/meilisearch-auth/src/error.rs @@ -22,12 +22,14 @@ pub enum AuthControllerError { "`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 uuidv4 string or ommited.")] + #[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}` already exists for an API key.")] + #[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), } @@ -51,6 +53,7 @@ impl ErrorCode for AuthControllerError { 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 baac68637..f6ff7096c 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -99,6 +99,30 @@ impl Key { self.name = des?; } + if value.get("uid").is_some() { + return Err(AuthControllerError::ImmutableField("uid".to_string())); + } + + 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(); Ok(()) diff --git a/meilisearch-error/src/lib.rs b/meilisearch-error/src/lib.rs index 57882f8e0..6e6273db2 100644 --- a/meilisearch-error/src/lib.rs +++ b/meilisearch-error/src/lib.rs @@ -168,6 +168,7 @@ pub enum Code { InvalidApiKeyDescription, InvalidApiKeyName, InvalidApiKeyUid, + ImmutableField, ApiKeyAlreadyExists, } @@ -278,6 +279,7 @@ impl Code { 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/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index 2d6f07d86..a9f2bf91d 100644 --- a/meilisearch-http/tests/auth/api_keys.rs +++ b/meilisearch-http/tests/auth/api_keys.rs @@ -470,7 +470,7 @@ async fn error_add_api_key_invalid_parameters_uid() { assert_eq!(400, code, "{:?}", &response); let expected_response = json!({ - "message": r#"`uid` field value `"aaaaabbbbbccc"` is invalid. It should be a valid uuidv4 string or ommited."#, + "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" @@ -499,7 +499,7 @@ async fn error_add_api_key_parameters_uid_already_exist() { assert_eq!(409, code, "{:?}", &response); let expected_response = json!({ - "message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` already exists for an API key.", + "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" @@ -1112,7 +1112,7 @@ async fn patch_api_key_name() { } #[actix_rt::test] -async fn patch_api_key_indexes_unchanged() { +async fn error_patch_api_key_indexes() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1143,44 +1143,24 @@ async fn patch_api_key_indexes_unchanged() { 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(); let content = json!({ "indexes": ["products", "prices"] }); 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); + 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": "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); } #[actix_rt::test] -async fn patch_api_key_actions_unchanged() { +async fn error_patch_api_key_actions() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1212,9 +1192,6 @@ async fn patch_api_key_actions_unchanged() { let uid = response["uid"].as_str().unwrap(); - let created_at = response["createdAt"].as_str().unwrap(); - let updated_at = response["updatedAt"].as_str().unwrap(); - let content = json!({ "actions": [ "search", @@ -1227,37 +1204,19 @@ async fn patch_api_key_actions_unchanged() { 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); + 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": "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); } #[actix_rt::test] -async fn patch_api_key_expiration_date_unchanged() { +async fn error_patch_api_key_expiration_date() { let mut server = Server::new_auth().await; server.use_api_key("MASTER_KEY"); @@ -1288,37 +1247,17 @@ async fn patch_api_key_expiration_date_unchanged() { 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(); let content = json!({ "expiresAt": "2055-11-13T00:00:00Z" }); 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); + 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": "2050-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); diff --git a/meilisearch-http/tests/auth/authorization.rs b/meilisearch-http/tests/auth/authorization.rs index fde4c61f3..2080e2990 100644 --- a/meilisearch-http/tests/auth/authorization.rs +++ b/meilisearch-http/tests/auth/authorization.rs @@ -149,31 +149,22 @@ 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!(201, code, "{:?}", &response); - 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 (response, code) = server.patch_api_key(&key, content).await; - assert_eq!(200, code, "{:?}", &response); + 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; From 7652295d2c9675da2b3b5297b115478739aff950 Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 1 Jun 2022 15:50:13 +0200 Subject: [PATCH 17/18] Encode key in base64 instead of hexa --- Cargo.lock | 1 + meilisearch-auth/Cargo.toml | 1 + meilisearch-auth/src/store.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f48e6c59d..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", diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index 7ffa072e8..dafeeef05 100644 --- a/meilisearch-auth/Cargo.toml +++ b/meilisearch-auth/Cargo.toml @@ -11,6 +11,7 @@ rand = "0.8.4" serde = { version = "1.0.136", features = ["derive"] } serde_json = { version = "1.0.79", features = ["preserve_order"] } sha2 = "0.10.2" +base64 = "0.13.0" 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/store.rs b/meilisearch-auth/src/store.rs index 762e707bc..64cf49544 100644 --- a/meilisearch-auth/src/store.rs +++ b/meilisearch-auth/src/store.rs @@ -238,7 +238,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec { pub fn generate_key(uid: &[u8], master_key: &[u8]) -> String { let key = [uid, master_key].concat(); let sha = Sha256::digest(&key); - format!("{:x}", sha) + 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. From 4512eed8f53590397dd23695010ae03168ed58ee Mon Sep 17 00:00:00 2001 From: ManyTheFish Date: Wed, 1 Jun 2022 18:06:20 +0200 Subject: [PATCH 18/18] Fix PR comments --- meilisearch-auth/Cargo.toml | 2 +- meilisearch-auth/src/key.rs | 2 +- meilisearch-auth/src/lib.rs | 19 ++++++++++--------- meilisearch-auth/src/store.rs | 12 +++++++++--- .../src/extractors/authentication/mod.rs | 2 +- meilisearch-http/src/routes/api_key.rs | 9 ++++++--- meilisearch-http/tests/auth/api_keys.rs | 2 +- 7 files changed, 29 insertions(+), 19 deletions(-) diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index dafeeef05..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" } @@ -11,7 +12,6 @@ rand = "0.8.4" serde = { version = "1.0.136", features = ["derive"] } serde_json = { version = "1.0.79", features = ["preserve_order"] } sha2 = "0.10.2" -base64 = "0.13.0" 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/key.rs b/meilisearch-auth/src/key.rs index f6ff7096c..0e336a7db 100644 --- a/meilisearch-auth/src/key.rs +++ b/meilisearch-auth/src/key.rs @@ -133,7 +133,7 @@ impl Key { let uid = Uuid::new_v4(); Self { name: Some("Default Admin API Key".to_string()), - description: Some("Use it for all other than search operations. Caution! Do not expose it on a public frontend".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()], diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 578093abf..e41fd92f4 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -4,20 +4,19 @@ pub mod error; mod key; mod store; -use crate::store::generate_key; use std::collections::{HashMap, HashSet}; use std::path::Path; - use std::sync::Arc; -use uuid::Uuid; use serde::{Deserialize, Serialize}; use serde_json::Value; 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; @@ -63,16 +62,18 @@ impl AuthController { .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string())) } - pub fn get_optional_uid_from_sha(&self, sha: &[u8]) -> Result> { + 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_sha(sha, master_key.as_bytes()), + Some(master_key) => self + .store + .get_uid_from_encoded_key(encoded_key, master_key.as_bytes()), None => Ok(None), } } - pub fn get_uid_from_sha(&self, sha: &str) -> Result { - self.get_optional_uid_from_sha(sha.as_bytes())? - .ok_or_else(|| AuthControllerError::ApiKeyNotFound(sha.to_string())) + 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( @@ -134,7 +135,7 @@ impl AuthController { pub fn generate_key(&self, uid: Uuid) -> Option { self.master_key .as_ref() - .map(|master_key| generate_key(uid.as_bytes(), master_key.as_bytes())) + .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 diff --git a/meilisearch-auth/src/store.rs b/meilisearch-auth/src/store.rs index 64cf49544..69c4cbd57 100644 --- a/meilisearch-auth/src/store.rs +++ b/meilisearch-auth/src/store.rs @@ -118,14 +118,20 @@ impl HeedAuthStore { self.keys.get(&rtxn, uid.as_bytes()).map_err(|e| e.into()) } - pub fn get_uid_from_sha(&self, key_sha: &[u8], master_key: &[u8]) -> 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(uid, master_key).as_bytes() == key_sha => { + 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)) } @@ -235,7 +241,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec { } } -pub fn generate_key(uid: &[u8], master_key: &[u8]) -> String { +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) diff --git a/meilisearch-http/src/extractors/authentication/mod.rs b/meilisearch-http/src/extractors/authentication/mod.rs index 99f972984..7732bd7fa 100644 --- a/meilisearch-http/src/extractors/authentication/mod.rs +++ b/meilisearch-http/src/extractors/authentication/mod.rs @@ -188,7 +188,7 @@ pub mod policies { return Some(filters); } else if let Some(action) = Action::from_repr(A) { // API key - if let Ok(Some(uid)) = auth.get_optional_uid_from_sha(token.as_bytes()) { + 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(); } diff --git a/meilisearch-http/src/routes/api_key.rs b/meilisearch-http/src/routes/api_key.rs index cfe81b301..831a350d8 100644 --- a/meilisearch-http/src/routes/api_key.rs +++ b/meilisearch-http/src/routes/api_key.rs @@ -69,7 +69,8 @@ pub async fn get_api_key( let key = path.into_inner().key; let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&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)) @@ -88,7 +89,8 @@ pub async fn patch_api_key( let key = path.into_inner().key; let body = body.into_inner(); let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&key))?; + 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)) @@ -105,7 +107,8 @@ pub async fn delete_api_key( ) -> Result { let key = path.into_inner().key; tokio::task::spawn_blocking(move || { - let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_sha(&key))?; + let uid = + Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?; auth_controller.delete_key(uid) }) .await diff --git a/meilisearch-http/tests/auth/api_keys.rs b/meilisearch-http/tests/auth/api_keys.rs index a9f2bf91d..4eb1fdd6f 100644 --- a/meilisearch-http/tests/auth/api_keys.rs +++ b/meilisearch-http/tests/auth/api_keys.rs @@ -711,7 +711,7 @@ async fn list_api_keys() { }, { "name": "Default Admin API Key", - "description": "Use it for all other than search operations. Caution! Do not expose it on a public frontend", + "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,