2025: Fix security index creation r=ManyTheFish a=ManyTheFish

Forbid index creation on alternates routes when the action `index.create` is not given

fix #2024

Co-authored-by: Maxime Legendre <maximelegendre@MacBook-Pro-de-Maxime.local>
This commit is contained in:
bors[bot] 2021-12-20 14:04:28 +00:00 committed by GitHub
commit eaff393c76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 213 additions and 23 deletions

View File

@ -69,6 +69,11 @@ impl AuthController {
if !key.indexes.iter().any(|i| i.as_str() == "*") { if !key.indexes.iter().any(|i| i.as_str() == "*") {
filters.indexes = Some(key.indexes); filters.indexes = Some(key.indexes);
} }
filters.allow_index_creation = key
.actions
.iter()
.any(|&action| action == Action::IndexesAdd || action == Action::All);
} }
Ok(filters) Ok(filters)
@ -118,9 +123,18 @@ impl AuthController {
} }
} }
#[derive(Default)]
pub struct AuthFilter { pub struct AuthFilter {
pub indexes: Option<Vec<String>>, pub indexes: Option<Vec<String>>,
pub allow_index_creation: bool,
}
impl Default for AuthFilter {
fn default() -> Self {
Self {
indexes: None,
allow_index_creation: true,
}
}
} }
pub fn generate_key(master_key: &[u8], uid: &str) -> String { pub fn generate_key(master_key: &[u8], uid: &str) -> String {

View File

@ -173,6 +173,7 @@ pub async fn add_documents(
&req, &req,
); );
let allow_index_creation = meilisearch.filters().allow_index_creation;
let task = document_addition( let task = document_addition(
extract_mime_type(&req)?, extract_mime_type(&req)?,
meilisearch, meilisearch,
@ -180,6 +181,7 @@ pub async fn add_documents(
params.primary_key, params.primary_key,
body, body,
IndexDocumentsMethod::ReplaceDocuments, IndexDocumentsMethod::ReplaceDocuments,
allow_index_creation,
) )
.await?; .await?;
@ -203,6 +205,7 @@ pub async fn update_documents(
&req, &req,
); );
let allow_index_creation = meilisearch.filters().allow_index_creation;
let task = document_addition( let task = document_addition(
extract_mime_type(&req)?, extract_mime_type(&req)?,
meilisearch, meilisearch,
@ -210,6 +213,7 @@ pub async fn update_documents(
params.into_inner().primary_key, params.into_inner().primary_key,
body, body,
IndexDocumentsMethod::UpdateDocuments, IndexDocumentsMethod::UpdateDocuments,
allow_index_creation,
) )
.await?; .await?;
@ -223,6 +227,7 @@ async fn document_addition(
primary_key: Option<String>, primary_key: Option<String>,
body: Payload, body: Payload,
method: IndexDocumentsMethod, method: IndexDocumentsMethod,
allow_index_creation: bool,
) -> Result<SummarizedTaskView, ResponseError> { ) -> Result<SummarizedTaskView, ResponseError> {
let format = match mime_type let format = match mime_type
.as_ref() .as_ref()
@ -250,6 +255,7 @@ async fn document_addition(
primary_key, primary_key,
method, method,
format, format,
allow_index_creation,
}; };
let task = meilisearch.register_update(index_uid, update).await?.into(); let task = meilisearch.register_update(index_uid, update).await?.into();

View File

@ -34,9 +34,12 @@ macro_rules! make_setting_route {
$attr: Setting::Reset, $attr: Setting::Reset,
..Default::default() ..Default::default()
}; };
let allow_index_creation = meilisearch.filters().allow_index_creation;
let update = Update::Settings { let update = Update::Settings {
settings, settings,
is_deletion: true, is_deletion: true,
allow_index_creation,
}; };
let task: SummarizedTaskView = meilisearch let task: SummarizedTaskView = meilisearch
.register_update(index_uid.into_inner(), update) .register_update(index_uid.into_inner(), update)
@ -66,9 +69,11 @@ macro_rules! make_setting_route {
..Default::default() ..Default::default()
}; };
let allow_index_creation = meilisearch.filters().allow_index_creation;
let update = Update::Settings { let update = Update::Settings {
settings, settings,
is_deletion: false, is_deletion: false,
allow_index_creation,
}; };
let task: SummarizedTaskView = meilisearch let task: SummarizedTaskView = meilisearch
.register_update(index_uid.into_inner(), update) .register_update(index_uid.into_inner(), update)
@ -272,9 +277,11 @@ pub async fn update_all(
Some(&req), Some(&req),
); );
let allow_index_creation = meilisearch.filters().allow_index_creation;
let update = Update::Settings { let update = Update::Settings {
settings, settings,
is_deletion: false, is_deletion: false,
allow_index_creation,
}; };
let task: SummarizedTaskView = meilisearch let task: SummarizedTaskView = meilisearch
.register_update(index_uid.into_inner(), update) .register_update(index_uid.into_inner(), update)
@ -300,9 +307,11 @@ pub async fn delete_all(
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let settings = Settings::cleared().into_unchecked(); let settings = Settings::cleared().into_unchecked();
let allow_index_creation = data.filters().allow_index_creation;
let update = Update::Settings { let update = Update::Settings {
settings, settings,
is_deletion: true, is_deletion: true,
allow_index_creation,
}; };
let task: SummarizedTaskView = data let task: SummarizedTaskView = data
.register_update(index_uid.into_inner(), update) .register_update(index_uid.into_inner(), update)

View File

@ -496,3 +496,135 @@ async fn list_authorized_tasks_no_index_restriction() {
// key should have access on `test` index. // key should have access on `test` index.
assert!(response.iter().any(|task| task["indexUid"] == "test")); assert!(response.iter().any(|task| task["indexUid"] == "test"));
} }
#[actix_rt::test]
async fn error_creating_index_without_action() {
let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY");
// create key with access on all indexes.
let content = json!({
"indexes": ["*"],
"actions": ALL_ACTIONS.iter().cloned().filter(|a| *a != "indexes.create").collect::<Vec<_>>(),
"expiresAt": "2050-11-13T00:00:00Z"
});
let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201);
assert!(response["key"].is_string());
// use created key.
let key = response["key"].as_str().unwrap();
server.use_api_key(&key);
let expected_error = json!({
"message": "Index `test` not found.",
"code": "index_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#index_not_found"
});
// try to create a index via add documents route
let index = server.index("test");
let documents = json!([
{
"id": 1,
"content": "foo",
}
]);
let (response, code) = index.add_documents(documents, None).await;
assert_eq!(code, 202, "{:?}", response);
let task_id = response["uid"].as_u64().unwrap();
let response = index.wait_task(task_id).await;
assert_eq!(response["status"], "failed");
assert_eq!(response["error"], expected_error.clone());
// try to create a index via add settings route
let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await;
assert_eq!(code, 202);
let task_id = response["uid"].as_u64().unwrap();
let response = index.wait_task(task_id).await;
assert_eq!(response["status"], "failed");
assert_eq!(response["error"], expected_error.clone());
// try to create a index via add specialized settings route
let (response, code) = index.update_distinct_attribute(json!("test")).await;
assert_eq!(code, 202);
let task_id = response["uid"].as_u64().unwrap();
let response = index.wait_task(task_id).await;
assert_eq!(response["status"], "failed");
assert_eq!(response["error"], expected_error.clone());
}
#[actix_rt::test]
async fn lazy_create_index() {
let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY");
// create key with access on all indexes.
let content = json!({
"indexes": ["*"],
"actions": ["*"],
"expiresAt": "2050-11-13T00:00:00Z"
});
let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201);
assert!(response["key"].is_string());
// use created key.
let key = response["key"].as_str().unwrap();
server.use_api_key(&key);
// try to create a index via add documents route
let index = server.index("test");
let documents = json!([
{
"id": 1,
"content": "foo",
}
]);
let (response, code) = index.add_documents(documents, None).await;
assert_eq!(code, 202, "{:?}", response);
let task_id = response["uid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(code, 200);
assert_eq!(response["status"], "succeeded");
// try to create a index via add settings route
let index = server.index("test1");
let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await;
assert_eq!(code, 202);
let task_id = response["uid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(code, 200);
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);
let task_id = response["uid"].as_u64().unwrap();
index.wait_task(task_id).await;
let (response, code) = index.get_task(task_id).await;
assert_eq!(code, 200);
assert_eq!(response["status"], "succeeded");
}

View File

@ -2,29 +2,12 @@ mod api_keys;
mod authorization; mod authorization;
mod payload; mod payload;
use crate::common::server::default_settings;
use crate::common::server::TEST_TEMP_DIR;
use crate::common::Server; use crate::common::Server;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tempfile::TempDir;
impl Server { impl Server {
pub async fn new_auth() -> Self {
let dir = TempDir::new().unwrap();
if cfg!(windows) {
std::env::set_var("TMP", TEST_TEMP_DIR.path());
} else {
std::env::set_var("TMPDIR", TEST_TEMP_DIR.path());
}
let mut options = default_settings(dir.path());
options.master_key = Some("MASTER_KEY".to_string());
Self::new_with_options(options).await
}
pub fn use_api_key(&mut self, api_key: impl AsRef<str>) { pub fn use_api_key(&mut self, api_key: impl AsRef<str>) {
self.service.api_key = Some(api_key.as_ref().to_string()); self.service.api_key = Some(api_key.as_ref().to_string());
} }

View File

@ -50,6 +50,33 @@ impl Server {
} }
} }
pub async fn new_auth() -> Self {
let dir = TempDir::new().unwrap();
if cfg!(windows) {
std::env::set_var("TMP", TEST_TEMP_DIR.path());
} else {
std::env::set_var("TMPDIR", TEST_TEMP_DIR.path());
}
let mut options = default_settings(dir.path());
options.master_key = Some("MASTER_KEY".to_string());
let meilisearch = setup_meilisearch(&options).unwrap();
let auth = AuthController::new(&options.db_path, &options.master_key).unwrap();
let service = Service {
meilisearch,
auth,
options,
api_key: None,
};
Server {
service,
_dir: Some(dir),
}
}
pub async fn new_with_options(options: Opt) -> Self { pub async fn new_with_options(options: Opt) -> Self {
let meilisearch = setup_meilisearch(&options).unwrap(); let meilisearch = setup_meilisearch(&options).unwrap();
let auth = AuthController::new(&options.db_path, &options.master_key).unwrap(); let auth = AuthController::new(&options.db_path, &options.master_key).unwrap();

View File

@ -17,3 +17,4 @@ cc 3a01c78db082434b8a4f8914abf0d1059d39f4426d16df20d72e1bd7ebb94a6a # shrinks to
cc c450806df3921d1e6fe9b6af93d999e8196d0175b69b64f1810802582421e94a # shrinks to task = Task { id: 0, index_uid: IndexUid("a"), content: CreateIndex { primary_key: Some("") }, events: [] }, index_exists = false, index_op_fails = false, any_int = 0 cc c450806df3921d1e6fe9b6af93d999e8196d0175b69b64f1810802582421e94a # shrinks to task = Task { id: 0, index_uid: IndexUid("a"), content: CreateIndex { primary_key: Some("") }, events: [] }, index_exists = false, index_op_fails = false, any_int = 0
cc fb6b98947cbdbdee05ed3c0bf2923aad2c311edc276253642eb43a0c0ec4888a # shrinks to task = Task { id: 0, index_uid: IndexUid("A"), content: CreateIndex { primary_key: Some("") }, events: [] }, index_exists = false, index_op_fails = true, any_int = 0 cc fb6b98947cbdbdee05ed3c0bf2923aad2c311edc276253642eb43a0c0ec4888a # shrinks to task = Task { id: 0, index_uid: IndexUid("A"), content: CreateIndex { primary_key: Some("") }, events: [] }, index_exists = false, index_op_fails = true, any_int = 0
cc 1aa59d8e22484e9915efbb5818e1e1ab684aa61b166dc82130d6221663ba00bf # shrinks to task = Task { id: 0, index_uid: IndexUid("a"), content: DocumentDeletion(Clear), events: [] }, index_exists = true, index_op_fails = false, any_int = 0 cc 1aa59d8e22484e9915efbb5818e1e1ab684aa61b166dc82130d6221663ba00bf # shrinks to task = Task { id: 0, index_uid: IndexUid("a"), content: DocumentDeletion(Clear), events: [] }, index_exists = true, index_op_fails = false, any_int = 0
cc 2e8644e6397b5f76e0b79f961fa125e2f45f42f26e03c453c9a174dfb427500d # shrinks to task = Task { id: 0, index_uid: IndexUid("0"), content: SettingsUpdate { settings: Settings { displayed_attributes: NotSet, searchable_attributes: NotSet, filterable_attributes: NotSet, sortable_attributes: NotSet, ranking_rules: NotSet, stop_words: NotSet, synonyms: NotSet, distinct_attribute: NotSet, _kind: PhantomData }, is_deletion: false, allow_index_creation: false }, events: [] }, index_exists = false, index_op_fails = false, any_int = 0

View File

@ -74,11 +74,13 @@ impl From<Update> for TaskContent {
primary_key, primary_key,
// document count is unknown for legacy updates // document count is unknown for legacy updates
documents_count: 0, documents_count: 0,
allow_index_creation: true,
}, },
Update::Settings(settings) => TaskContent::SettingsUpdate { Update::Settings(settings) => TaskContent::SettingsUpdate {
settings, settings,
// There is no way to know now, so we assume it isn't // There is no way to know now, so we assume it isn't
is_deletion: false, is_deletion: false,
allow_index_creation: true,
}, },
Update::ClearDocuments => TaskContent::DocumentDeletion(DocumentDeletion::Clear), Update::ClearDocuments => TaskContent::DocumentDeletion(DocumentDeletion::Clear),
} }

View File

@ -119,6 +119,7 @@ pub enum Update {
settings: Settings<Unchecked>, settings: Settings<Unchecked>,
/// Indicates whether the update was a deletion /// Indicates whether the update was a deletion
is_deletion: bool, is_deletion: bool,
allow_index_creation: bool,
}, },
DocumentAddition { DocumentAddition {
#[derivative(Debug = "ignore")] #[derivative(Debug = "ignore")]
@ -126,6 +127,7 @@ pub enum Update {
primary_key: Option<String>, primary_key: Option<String>,
method: IndexDocumentsMethod, method: IndexDocumentsMethod,
format: DocumentAdditionFormat, format: DocumentAdditionFormat,
allow_index_creation: bool,
}, },
DeleteIndex, DeleteIndex,
CreateIndex { CreateIndex {
@ -340,15 +342,18 @@ where
Update::Settings { Update::Settings {
settings, settings,
is_deletion, is_deletion,
allow_index_creation,
} => TaskContent::SettingsUpdate { } => TaskContent::SettingsUpdate {
settings, settings,
is_deletion, is_deletion,
allow_index_creation,
}, },
Update::DocumentAddition { Update::DocumentAddition {
mut payload, mut payload,
primary_key, primary_key,
format, format,
method, method,
allow_index_creation,
} => { } => {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
while let Some(bytes) = payload.next().await { while let Some(bytes) = payload.next().await {
@ -380,6 +385,7 @@ where
merge_strategy: method, merge_strategy: method,
primary_key, primary_key,
documents_count, documents_count,
allow_index_creation,
} }
} }
Update::DeleteIndex => TaskContent::IndexDeletion, Update::DeleteIndex => TaskContent::IndexDeletion,

View File

@ -188,13 +188,18 @@ where
content_uuid, content_uuid,
merge_strategy, merge_strategy,
primary_key, primary_key,
allow_index_creation,
.. ..
} => { } => {
let primary_key = primary_key.clone(); let primary_key = primary_key.clone();
let content_uuid = *content_uuid; let content_uuid = *content_uuid;
let method = *merge_strategy; let method = *merge_strategy;
let index = self.get_or_create_index(index_uid, task.id).await?; let index = if *allow_index_creation {
self.get_or_create_index(index_uid, task.id).await?
} else {
self.get_index(index_uid.into_inner()).await?
};
let file_store = self.file_store.clone(); let file_store = self.file_store.clone();
let result = spawn_blocking(move || { let result = spawn_blocking(move || {
index.update_documents(method, content_uuid, primary_key, file_store) index.update_documents(method, content_uuid, primary_key, file_store)
@ -227,8 +232,9 @@ where
TaskContent::SettingsUpdate { TaskContent::SettingsUpdate {
settings, settings,
is_deletion, is_deletion,
allow_index_creation,
} => { } => {
let index = if *is_deletion { let index = if *is_deletion || !*allow_index_creation {
self.get_index(index_uid.into_inner()).await? self.get_index(index_uid.into_inner()).await?
} else { } else {
self.get_or_create_index(index_uid, task.id).await? self.get_or_create_index(index_uid, task.id).await?
@ -503,8 +509,8 @@ mod test {
match &task.content { match &task.content {
// an unexisting index should trigger an index creation in the folllowing cases: // an unexisting index should trigger an index creation in the folllowing cases:
TaskContent::DocumentAddition { .. } TaskContent::DocumentAddition { allow_index_creation: true, .. }
| TaskContent::SettingsUpdate { is_deletion: false, .. } | TaskContent::SettingsUpdate { allow_index_creation: true, is_deletion: false, .. }
| TaskContent::IndexCreation { .. } if !index_exists => { | TaskContent::IndexCreation { .. } if !index_exists => {
index_store index_store
.expect_create() .expect_create()
@ -566,6 +572,8 @@ mod test {
|| (!index_exists && matches!(task.content, TaskContent::IndexDeletion || (!index_exists && matches!(task.content, TaskContent::IndexDeletion
| TaskContent::DocumentDeletion(_) | TaskContent::DocumentDeletion(_)
| TaskContent::SettingsUpdate { is_deletion: true, ..} | TaskContent::SettingsUpdate { is_deletion: true, ..}
| TaskContent::SettingsUpdate { allow_index_creation: false, ..}
| TaskContent::DocumentAddition { allow_index_creation: false, ..}
| TaskContent::IndexUpdate { .. } )) | TaskContent::IndexUpdate { .. } ))
{ {
assert!(result.is_err(), "{:?}", result); assert!(result.is_err(), "{:?}", result);

View File

@ -134,12 +134,14 @@ pub enum TaskContent {
merge_strategy: IndexDocumentsMethod, merge_strategy: IndexDocumentsMethod,
primary_key: Option<String>, primary_key: Option<String>,
documents_count: usize, documents_count: usize,
allow_index_creation: bool,
}, },
DocumentDeletion(DocumentDeletion), DocumentDeletion(DocumentDeletion),
SettingsUpdate { SettingsUpdate {
settings: Settings<Unchecked>, settings: Settings<Unchecked>,
/// Indicates whether the task was a deletion /// Indicates whether the task was a deletion
is_deletion: bool, is_deletion: bool,
allow_index_creation: bool,
}, },
IndexDeletion, IndexDeletion,
IndexCreation { IndexCreation {