implements more route

This commit is contained in:
Tamo 2024-10-09 14:22:56 +02:00
parent 143a866b48
commit e9d74d424b
5 changed files with 166 additions and 19 deletions

View File

@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time}; use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError}; use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError};
@ -32,19 +33,31 @@ impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for Dese
} }
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct CreateApiKey { pub struct CreateApiKey {
/// A description for the key. `null` if empty.
#[schema(example = json!(null))]
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
pub description: Option<String>, pub description: Option<String>,
/// A human-readable name for the key. `null` if empty.
#[schema(example = "Indexing Products API key")]
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
pub name: Option<String>, pub name: Option<String>,
/// A uuid v4 to identify the API Key. If not specified, it's generated by Meilisearch.
#[schema(value_type = Uuid, example = json!(null))]
#[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)] #[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)]
pub uid: KeyId, pub uid: KeyId,
/// A list of actions permitted for the key. `["*"]` for all actions. The `*` character can be used as a wildcard when located at the last position. e.g. `documents.*` to authorize access on all documents endpoints.
#[schema(example = json!(["documents.add"]))]
#[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)] #[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
pub actions: Vec<Action>, pub actions: Vec<Action>,
/// A list of accesible indexes permitted for the key. `["*"]` for all indexes. The `*` character can be used as a wildcard when located at the last position. e.g. `products_*` to allow access to all indexes whose names start with `products_`.
#[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)] #[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
#[schema(value_type = Vec<String>, example = json!(["products"]))]
pub indexes: Vec<IndexUidPattern>, pub indexes: Vec<IndexUidPattern>,
/// Represent the expiration date and time as RFC 3339 format. `null` equals to no expiration time.
#[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] #[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
} }
@ -179,7 +192,9 @@ fn parse_expiration_date(
} }
} }
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)] #[derive(
Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr, ToSchema,
)]
#[repr(u8)] #[repr(u8)]
pub enum Action { pub enum Action {
#[serde(rename = "*")] #[serde(rename = "*")]

View File

@ -150,7 +150,7 @@ pub enum KindWithContent {
SnapshotCreation, SnapshotCreation,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IndexSwap { pub struct IndexSwap {
pub indexes: (String, String), pub indexes: (String, String),

View File

@ -13,6 +13,7 @@ use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey}; use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use utoipa::{IntoParams, OpenApi, ToSchema};
use uuid::Uuid; use uuid::Uuid;
use super::PAGINATION_DEFAULT_LIMIT; use super::PAGINATION_DEFAULT_LIMIT;
@ -21,6 +22,20 @@ use crate::extractors::authentication::GuardedData;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::Pagination; use crate::routes::Pagination;
#[derive(OpenApi)]
#[openapi(
paths(create_api_key, list_api_keys),
tags((
name = "Keys",
description = "Manage API `keys` for a Meilisearch instance. Each key has a given set of permissions.
You must have the master key or the default admin key to access the keys route. More information about the keys and their rights.
Accessing any route under `/keys` without having set a master key will result in an error.",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/keys"),
)),
)]
pub struct ApiKeyApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
@ -35,6 +50,52 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
/// Create an API Key
///
/// Create an API Key.
#[utoipa::path(
post,
path = "/",
tag = "Keys",
security(("Bearer" = ["keys.create", "keys.*", "*"])),
request_body = CreateApiKey,
responses(
(status = 202, description = "Key has been created", body = KeyView, content_type = "application/json", example = json!(
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "Indexing Products API key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"products"
],
"expiresAt": "2021-11-13T00:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
"code": "missing_master_key",
"type": "auth",
"link": "https://docs.meilisearch.com/errors#missing_master_key"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "The Authorization header is missing. It must use the bearer authorization method.",
"code": "missing_authorization_header",
"type": "auth",
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
}
)),
)
)]
pub async fn create_api_key( pub async fn create_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
body: AwebJson<CreateApiKey, DeserrJsonError>, body: AwebJson<CreateApiKey, DeserrJsonError>,
@ -51,11 +112,14 @@ pub async fn create_api_key(
Ok(HttpResponse::Created().json(res)) Ok(HttpResponse::Created().json(res))
} }
#[derive(Deserr, Debug, Clone, Copy)] #[derive(Deserr, Debug, Clone, Copy, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct ListApiKeys { pub struct ListApiKeys {
#[into_params(value_type = usize, default = 0)]
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)] #[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
pub offset: Param<usize>, pub offset: Param<usize>,
#[into_params(value_type = usize, default = PAGINATION_DEFAULT_LIMIT)]
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)] #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
pub limit: Param<usize>, pub limit: Param<usize>,
} }
@ -66,6 +130,59 @@ impl ListApiKeys {
} }
} }
/// Get API Keys
///
/// List all API Keys
#[utoipa::path(
get,
path = "/",
tag = "Keys",
security(("Bearer" = ["keys.get", "keys.*", "*"])),
params(ListApiKeys),
responses(
(status = 202, description = "List of keys", body = PaginationView<KeyView>, content_type = "application/json", example = json!(
{
"results": [
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "An API Key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"movies"
],
"expiresAt": "2022-11-12T10:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
"code": "missing_master_key",
"type": "auth",
"link": "https://docs.meilisearch.com/errors#missing_master_key"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "The Authorization header is missing. It must use the bearer authorization method.",
"code": "missing_authorization_header",
"type": "auth",
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
}
)),
)
)]
pub async fn list_api_keys( pub async fn list_api_keys(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>, list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
@ -144,19 +261,28 @@ pub struct AuthParam {
key: String, key: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct KeyView { pub(super) struct KeyView {
/// The name of the API Key if any
name: Option<String>, name: Option<String>,
/// The description of the API Key if any
description: Option<String>, description: Option<String>,
/// The actual API Key you can send to Meilisearch
key: String, key: String,
/// The `Uuid` specified while creating the key or autogenerated by Meilisearch.
uid: Uuid, uid: Uuid,
/// The actions accessible with this key.
actions: Vec<Action>, actions: Vec<Action>,
/// The indexes accessible with this key.
indexes: Vec<String>, indexes: Vec<String>,
/// The expiration date of the key. Once this timestamp is exceeded the key is not deleted but cannot be used anymore.
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")] #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
expires_at: Option<OffsetDateTime>, expires_at: Option<OffsetDateTime>,
/// The date of creation of this API Key.
#[serde(serialize_with = "time::serde::rfc3339::serialize")] #[serde(serialize_with = "time::serde::rfc3339::serialize")]
created_at: OffsetDateTime, created_at: OffsetDateTime,
/// The date of the last update made on this key.
#[serde(serialize_with = "time::serde::rfc3339::serialize")] #[serde(serialize_with = "time::serde::rfc3339::serialize")]
updated_at: OffsetDateTime, updated_at: OffsetDateTime,
} }

View File

@ -297,7 +297,8 @@ fn entry_stream(
2024-10-08T13:35:02.643750Z WARN HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: tracing_actix_web::middleware: Error encountered while processing the incoming HTTP request: ResponseError { code: 400, message: "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625", error_code: "feature_not_enabled", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#feature_not_enabled" } 2024-10-08T13:35:02.643750Z WARN HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: tracing_actix_web::middleware: Error encountered while processing the incoming HTTP request: ResponseError { code: 400, message: "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625", error_code: "feature_not_enabled", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#feature_not_enabled" }
2024-10-08T13:35:02.644191Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: meilisearch: close time.busy=1.66ms time.idle=658µs 2024-10-08T13:35:02.644191Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: meilisearch: close time.busy=1.66ms time.idle=658µs
2024-10-08T13:35:18.564152Z INFO HTTP request{method=PATCH host="localhost:7700" route=/experimental-features query_parameters= user_agent=curl/8.6.0 status_code=200}: meilisearch: close time.busy=1.17ms time.idle=127µs 2024-10-08T13:35:18.564152Z INFO HTTP request{method=PATCH host="localhost:7700" route=/experimental-features query_parameters= user_agent=curl/8.6.0 status_code=200}: meilisearch: close time.busy=1.17ms time.idle=127µs
2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs "# 2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs
"#
)), )),
(status = 400, description = "The route is already being used", body = ResponseError, content_type = "application/json", example = json!( (status = 400, description = "The route is already being used", body = ResponseError, content_type = "application/json", example = json!(
{ {

View File

@ -10,6 +10,7 @@ use index_scheduler::IndexScheduler;
use meilisearch_auth::AuthController; use meilisearch_auth::AuthController;
use meilisearch_types::error::ErrorType; use meilisearch_types::error::ErrorType;
use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::keys::CreateApiKey;
use meilisearch_types::settings::Checked; use meilisearch_types::settings::Checked;
use meilisearch_types::settings::FacetingSettings; use meilisearch_types::settings::FacetingSettings;
use meilisearch_types::settings::MinWordSizeTyposSetting; use meilisearch_types::settings::MinWordSizeTyposSetting;
@ -27,6 +28,8 @@ use utoipa::ToSchema;
use utoipa_rapidoc::RapiDoc; use utoipa_rapidoc::RapiDoc;
use utoipa_scalar::{Scalar, Servable as ScalarServable}; use utoipa_scalar::{Scalar, Servable as ScalarServable};
use self::api_key::KeyView;
use self::api_key::ListApiKeys;
use self::indexes::IndexStats; use self::indexes::IndexStats;
use self::logs::GetLogs; use self::logs::GetLogs;
use self::logs::LogMode; use self::logs::LogMode;
@ -54,32 +57,33 @@ pub mod tasks;
(path = "/tasks", api = tasks::TaskApi), (path = "/tasks", api = tasks::TaskApi),
(path = "/snapshots", api = snapshot::SnapshotApi), (path = "/snapshots", api = snapshot::SnapshotApi),
(path = "/dumps", api = dump::DumpApi), (path = "/dumps", api = dump::DumpApi),
(path = "/keys", api = api_key::ApiKeyApi),
(path = "/metrics", api = metrics::MetricApi), (path = "/metrics", api = metrics::MetricApi),
(path = "/logs", api = logs::LogsApi), (path = "/logs", api = logs::LogsApi),
), ),
paths(get_health, get_version, get_stats), paths(get_health, get_version, get_stats),
modifiers(&OpenApiAuth), modifiers(&OpenApiAuth),
components(schemas(UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind)) components(schemas(PaginationView<KeyView>, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind))
)] )]
pub struct MeilisearchApi; pub struct MeilisearchApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
let openapi = MeilisearchApi::openapi(); let openapi = MeilisearchApi::openapi();
cfg.service(web::scope("/tasks").configure(tasks::configure)) cfg.service(web::scope("/tasks").configure(tasks::configure)) // done
.service(Scalar::with_url("/scalar", openapi.clone())) .service(Scalar::with_url("/scalar", openapi.clone())) // done
.service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi).path("/rapidoc")) .service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi).path("/rapidoc")) // done
.service(web::resource("/health").route(web::get().to(get_health))) .service(web::resource("/health").route(web::get().to(get_health))) // done
.service(web::scope("/logs").configure(logs::configure)) .service(web::scope("/logs").configure(logs::configure)) // done
.service(web::scope("/keys").configure(api_key::configure)) .service(web::scope("/keys").configure(api_key::configure))
.service(web::scope("/dumps").configure(dump::configure)) .service(web::scope("/dumps").configure(dump::configure)) // done
.service(web::scope("/snapshots").configure(snapshot::configure)) .service(web::scope("/snapshots").configure(snapshot::configure)) // done
.service(web::resource("/stats").route(web::get().to(get_stats))) .service(web::resource("/stats").route(web::get().to(get_stats))) // done
.service(web::resource("/version").route(web::get().to(get_version))) .service(web::resource("/version").route(web::get().to(get_version))) // done
.service(web::scope("/indexes").configure(indexes::configure)) .service(web::scope("/indexes").configure(indexes::configure))
.service(web::scope("/multi-search").configure(multi_search::configure)) .service(web::scope("/multi-search").configure(multi_search::configure))
.service(web::scope("/swap-indexes").configure(swap_indexes::configure)) .service(web::scope("/swap-indexes").configure(swap_indexes::configure))
.service(web::scope("/metrics").configure(metrics::configure)) .service(web::scope("/metrics").configure(metrics::configure)) // done
.service(web::scope("/experimental-features").configure(features::configure)); .service(web::scope("/experimental-features").configure(features::configure));
} }
@ -167,7 +171,8 @@ pub struct Pagination {
pub limit: usize, pub limit: usize,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize, ToSchema)]
#[schema(rename_all = "camelCase")]
pub struct PaginationView<T> { pub struct PaginationView<T> {
pub results: Vec<T>, pub results: Vec<T>,
pub offset: usize, pub offset: usize,