2096: feat(auth): Tenant token r=Kerollmops a=ManyTheFish

Make meilisearch support JWT authentication signed with meilisearch API keys
using HS256, HS384 or HS512 algorithms.

Related spec: [specifications#89](https://github.com/meilisearch/specifications/pull/89) [rendered](https://github.com/meilisearch/specifications/blob/scoped-api-keys/text/0089-tenant-tokens.md)
Fix #1991 


Co-authored-by: ManyTheFish <many@meilisearch.com>
This commit is contained in:
bors[bot] 2022-01-27 10:38:41 +00:00 committed by GitHub
commit 622c15e825
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 980 additions and 109 deletions

View file

@ -3,7 +3,7 @@ use std::str;
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::SecondsFormat;
use meilisearch_auth::{generate_key, Action, AuthController, Key};
use meilisearch_auth::{Action, AuthController, Key};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -30,7 +30,7 @@ pub async fn create_api_key(
_req: HttpRequest,
) -> Result<HttpResponse, ResponseError> {
let key = auth_controller.create_key(body.into_inner()).await?;
let res = KeyView::from_key(key, auth_controller.get_master_key());
let res = KeyView::from_key(key, &auth_controller);
Ok(HttpResponse::Created().json(res))
}
@ -42,7 +42,7 @@ pub async fn list_api_keys(
let keys = auth_controller.list_keys().await?;
let res: Vec<_> = keys
.into_iter()
.map(|k| KeyView::from_key(k, auth_controller.get_master_key()))
.map(|k| KeyView::from_key(k, &auth_controller))
.collect();
Ok(HttpResponse::Ok().json(KeyListView::from(res)))
@ -52,9 +52,8 @@ pub async fn get_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>,
path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
// keep 8 first characters that are the ID of the API key.
let key = auth_controller.get_key(&path.api_key).await?;
let res = KeyView::from_key(key, auth_controller.get_master_key());
let res = KeyView::from_key(key, &auth_controller);
Ok(HttpResponse::Ok().json(res))
}
@ -65,10 +64,9 @@ pub async fn patch_api_key(
path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
let key = auth_controller
// keep 8 first characters that are the ID of the API key.
.update_key(&path.api_key, body.into_inner())
.await?;
let res = KeyView::from_key(key, auth_controller.get_master_key());
let res = KeyView::from_key(key, &auth_controller);
Ok(HttpResponse::Ok().json(res))
}
@ -77,7 +75,6 @@ pub async fn delete_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>,
path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
// keep 8 first characters that are the ID of the API key.
auth_controller.delete_key(&path.api_key).await?;
Ok(HttpResponse::NoContent().finish())
@ -101,12 +98,9 @@ struct KeyView {
}
impl KeyView {
fn from_key(key: Key, master_key: Option<&String>) -> Self {
fn from_key(key: Key, auth: &AuthController) -> Self {
let key_id = str::from_utf8(&key.id).unwrap();
let generated_key = match master_key {
Some(master_key) => generate_key(master_key.as_bytes(), key_id),
None => generate_key(&[], key_id),
};
let generated_key = auth.generate_key(key_id).unwrap_or_default();
KeyView {
description: key.description,

View file

@ -41,14 +41,13 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
pub async fn list_indexes(
data: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, MeiliSearch>,
) -> Result<HttpResponse, ResponseError> {
let filters = data.filters();
let mut indexes = data.list_indexes().await?;
if let Some(indexes_filter) = filters.indexes.as_ref() {
indexes = indexes
.into_iter()
.filter(|i| indexes_filter.contains(&i.uid))
.collect();
}
let search_rules = &data.filters().search_rules;
let indexes: Vec<_> = data
.list_indexes()
.await?
.into_iter()
.filter(|i| search_rules.is_index_authorized(&i.uid))
.collect();
debug!("returns: {:?}", indexes);
Ok(HttpResponse::Ok().json(indexes))

View file

@ -1,5 +1,6 @@
use actix_web::{web, HttpRequest, HttpResponse};
use log::debug;
use meilisearch_auth::IndexSearchRules;
use meilisearch_error::ResponseError;
use meilisearch_lib::index::{default_crop_length, SearchQuery, DEFAULT_SEARCH_LIMIT};
use meilisearch_lib::MeiliSearch;
@ -79,6 +80,26 @@ impl From<SearchQueryGet> for SearchQuery {
}
}
/// Incorporate search rules in search query
fn add_search_rules(query: &mut SearchQuery, rules: IndexSearchRules) {
query.filter = match (query.filter.take(), rules.filter) {
(None, rules_filter) => rules_filter,
(filter, None) => filter,
(Some(filter), Some(rules_filter)) => {
let filter = match filter {
Value::Array(filter) => filter,
filter => vec![filter],
};
let rules_filter = match rules_filter {
Value::Array(rules_filter) => rules_filter,
rules_filter => vec![rules_filter],
};
Some(Value::Array([filter, rules_filter].concat()))
}
}
}
// TODO: TAMO: split on :asc, and :desc, instead of doing some weird things
/// Transform the sort query parameter into something that matches the post expected format.
@ -113,11 +134,21 @@ pub async fn search_with_url_query(
analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> {
debug!("called with params: {:?}", params);
let query: SearchQuery = params.into_inner().into();
let mut query: SearchQuery = params.into_inner().into();
let index_uid = path.into_inner();
// Tenant token search_rules.
if let Some(search_rules) = meilisearch
.filters()
.search_rules
.get_index_search_rules(&index_uid)
{
add_search_rules(&mut query, search_rules);
}
let mut aggregate = SearchAggregator::from_query(&query, &req);
let search_result = meilisearch.search(path.into_inner(), query).await;
let search_result = meilisearch.search(index_uid, query).await;
if let Ok(ref search_result) = search_result {
aggregate.succeed(search_result);
}
@ -140,12 +171,22 @@ pub async fn search_with_post(
req: HttpRequest,
analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> {
let query = params.into_inner();
let mut query = params.into_inner();
debug!("search called with params: {:?}", query);
let index_uid = path.into_inner();
// Tenant token search_rules.
if let Some(search_rules) = meilisearch
.filters()
.search_rules
.get_index_search_rules(&index_uid)
{
add_search_rules(&mut query, search_rules);
}
let mut aggregate = SearchAggregator::from_query(&query, &req);
let search_result = meilisearch.search(path.into_inner(), query).await;
let search_result = meilisearch.search(index_uid, query).await;
if let Ok(ref search_result) = search_result {
aggregate.succeed(search_result);
}

View file

@ -127,9 +127,9 @@ pub async fn running() -> HttpResponse {
async fn get_stats(
meilisearch: GuardedData<ActionPolicy<{ actions::STATS_GET }>, MeiliSearch>,
) -> Result<HttpResponse, ResponseError> {
let filters = meilisearch.filters();
let search_rules = &meilisearch.filters().search_rules;
let response = meilisearch.get_all_stats(&filters.indexes).await?;
let response = meilisearch.get_all_stats(search_rules).await?;
debug!("returns: {:?}", response);
Ok(HttpResponse::Ok().json(response))

View file

@ -25,13 +25,16 @@ async fn get_tasks(
Some(&req),
);
let filters = meilisearch.filters().indexes.as_ref().map(|indexes| {
let search_rules = &meilisearch.filters().search_rules;
let filters = if search_rules.is_index_authorized("*") {
None
} else {
let mut filters = TaskFilter::default();
for index in indexes {
filters.filter_index(index.to_string());
for (index, _policy) in search_rules.clone() {
filters.filter_index(index);
}
filters
});
Some(filters)
};
let tasks: TaskListView = meilisearch
.list_tasks(filters, None, None)
@ -56,13 +59,16 @@ async fn get_task(
Some(&req),
);
let filters = meilisearch.filters().indexes.as_ref().map(|indexes| {
let search_rules = &meilisearch.filters().search_rules;
let filters = if search_rules.is_index_authorized("*") {
None
} else {
let mut filters = TaskFilter::default();
for index in indexes {
filters.filter_index(index.to_string());
for (index, _policy) in search_rules.clone() {
filters.filter_index(index);
}
filters
});
Some(filters)
};
let task: TaskView = meilisearch
.get_task(task_id.into_inner(), filters)