mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-07-16 19:30:43 +00:00
Merge pull request #5693 from Mubelotix/default-key
Add a Read-Only Admin API Key by default
This commit is contained in:
@ -158,7 +158,7 @@ impl AuthController {
|
|||||||
self.store.delete_all_keys()
|
self.store.delete_all_keys()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all the keys in the DB.
|
/// Insert a key directly into the store.
|
||||||
pub fn raw_insert_key(&mut self, key: Key) -> Result<()> {
|
pub fn raw_insert_key(&mut self, key: Key) -> Result<()> {
|
||||||
self.store.put_api_key(key)?;
|
self.store.put_api_key(key)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -351,6 +351,7 @@ pub struct IndexSearchRules {
|
|||||||
|
|
||||||
fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
|
fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
|
||||||
store.put_api_key(Key::default_chat())?;
|
store.put_api_key(Key::default_chat())?;
|
||||||
|
store.put_api_key(Key::default_read_only_admin())?;
|
||||||
store.put_api_key(Key::default_admin())?;
|
store.put_api_key(Key::default_admin())?;
|
||||||
store.put_api_key(Key::default_search())?;
|
store.put_api_key(Key::default_search())?;
|
||||||
|
|
||||||
|
@ -88,7 +88,13 @@ impl HeedAuthStore {
|
|||||||
let mut actions = HashSet::new();
|
let mut actions = HashSet::new();
|
||||||
for action in &key.actions {
|
for action in &key.actions {
|
||||||
match action {
|
match action {
|
||||||
Action::All => actions.extend(enum_iterator::all::<Action>()),
|
Action::All => {
|
||||||
|
actions.extend(enum_iterator::all::<Action>());
|
||||||
|
actions.remove(&Action::AllGet);
|
||||||
|
}
|
||||||
|
Action::AllGet => {
|
||||||
|
actions.extend(enum_iterator::all::<Action>().filter(|a| a.is_read()))
|
||||||
|
}
|
||||||
Action::DocumentsAll => {
|
Action::DocumentsAll => {
|
||||||
actions.extend(
|
actions.extend(
|
||||||
[Action::DocumentsGet, Action::DocumentsDelete, Action::DocumentsAdd]
|
[Action::DocumentsGet, Action::DocumentsDelete, Action::DocumentsAdd]
|
||||||
|
@ -144,6 +144,21 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_read_only_admin() -> Self {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let uid = Uuid::new_v4();
|
||||||
|
Self {
|
||||||
|
name: Some("Default Read-Only Admin API Key".to_string()),
|
||||||
|
description: Some("Use it to read information across the whole database. Caution! Do not expose this key on a public frontend".to_string()),
|
||||||
|
uid,
|
||||||
|
actions: vec![Action::AllGet, Action::KeysGet],
|
||||||
|
indexes: vec![IndexUidPattern::all()],
|
||||||
|
expires_at: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_search() -> Self {
|
pub fn default_search() -> Self {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
@ -218,6 +233,9 @@ pub enum Action {
|
|||||||
#[serde(rename = "*")]
|
#[serde(rename = "*")]
|
||||||
#[deserr(rename = "*")]
|
#[deserr(rename = "*")]
|
||||||
All = 0,
|
All = 0,
|
||||||
|
#[serde(rename = "*.get")]
|
||||||
|
#[deserr(rename = "*.get")]
|
||||||
|
AllGet,
|
||||||
#[serde(rename = "search")]
|
#[serde(rename = "search")]
|
||||||
#[deserr(rename = "search")]
|
#[deserr(rename = "search")]
|
||||||
Search,
|
Search,
|
||||||
@ -399,6 +417,52 @@ impl Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the action should be included in [Action::AllRead].
|
||||||
|
pub fn is_read(&self) -> bool {
|
||||||
|
use Action::*;
|
||||||
|
|
||||||
|
// It's using an exhaustive match to force the addition of new actions.
|
||||||
|
match self {
|
||||||
|
// Any action that expands to others must return false, as it wouldn't be able to expand recursively.
|
||||||
|
All | AllGet | DocumentsAll | IndexesAll | ChatsAll | TasksAll | SettingsAll
|
||||||
|
| StatsAll | MetricsAll | DumpsAll | SnapshotsAll | ChatsSettingsAll => false,
|
||||||
|
|
||||||
|
Search => true,
|
||||||
|
DocumentsAdd => false,
|
||||||
|
DocumentsGet => true,
|
||||||
|
DocumentsDelete => false,
|
||||||
|
Export => true,
|
||||||
|
IndexesAdd => false,
|
||||||
|
IndexesGet => true,
|
||||||
|
IndexesUpdate => false,
|
||||||
|
IndexesDelete => false,
|
||||||
|
IndexesSwap => false,
|
||||||
|
TasksCancel => false,
|
||||||
|
TasksDelete => false,
|
||||||
|
TasksGet => true,
|
||||||
|
SettingsGet => true,
|
||||||
|
SettingsUpdate => false,
|
||||||
|
StatsGet => true,
|
||||||
|
MetricsGet => true,
|
||||||
|
DumpsCreate => false,
|
||||||
|
SnapshotsCreate => false,
|
||||||
|
Version => true,
|
||||||
|
KeysAdd => false,
|
||||||
|
KeysGet => false, // Disabled in order to prevent privilege escalation
|
||||||
|
KeysUpdate => false,
|
||||||
|
KeysDelete => false,
|
||||||
|
ExperimentalFeaturesGet => true,
|
||||||
|
ExperimentalFeaturesUpdate => false,
|
||||||
|
NetworkGet => true,
|
||||||
|
NetworkUpdate => false,
|
||||||
|
ChatCompletions => false, // Disabled because it might trigger generation of new chats
|
||||||
|
ChatsGet => true,
|
||||||
|
ChatsDelete => false,
|
||||||
|
ChatsSettingsGet => true,
|
||||||
|
ChatsSettingsUpdate => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn repr(&self) -> u8 {
|
pub const fn repr(&self) -> u8 {
|
||||||
*self as u8
|
*self as u8
|
||||||
}
|
}
|
||||||
@ -408,6 +472,7 @@ pub mod actions {
|
|||||||
use super::Action::*;
|
use super::Action::*;
|
||||||
|
|
||||||
pub(crate) const ALL: u8 = All.repr();
|
pub(crate) const ALL: u8 = All.repr();
|
||||||
|
pub const ALL_GET: u8 = AllGet.repr();
|
||||||
pub const SEARCH: u8 = Search.repr();
|
pub const SEARCH: u8 = Search.repr();
|
||||||
pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
|
pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
|
||||||
pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
|
pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
|
||||||
|
@ -419,14 +419,14 @@ async fn error_add_api_key_invalid_parameters_actions() {
|
|||||||
let (response, code) = server.add_api_key(content).await;
|
let (response, code) = server.add_api_key(content).await;
|
||||||
|
|
||||||
meili_snap::snapshot!(code, @"400 Bad Request");
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
|
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r#"
|
||||||
{
|
{
|
||||||
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `export`, `network.get`, `network.update`, `chatCompletions`, `chats.*`, `chats.get`, `chats.delete`, `chatsSettings.*`, `chatsSettings.get`, `chatsSettings.update`",
|
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `*.get`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `export`, `network.get`, `network.update`, `chatCompletions`, `chats.*`, `chats.get`, `chats.delete`, `chatsSettings.*`, `chatsSettings.get`, `chatsSettings.update`",
|
||||||
"code": "invalid_api_key_actions",
|
"code": "invalid_api_key_actions",
|
||||||
"type": "invalid_request",
|
"type": "invalid_request",
|
||||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||||
}
|
}
|
||||||
"###);
|
"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
@ -790,7 +790,7 @@ async fn list_api_keys() {
|
|||||||
meili_snap::snapshot!(code, @"201 Created");
|
meili_snap::snapshot!(code, @"201 Created");
|
||||||
|
|
||||||
let (response, code) = server.list_api_keys("").await;
|
let (response, code) = server.list_api_keys("").await;
|
||||||
meili_snap::snapshot!(meili_snap::json_string!(response, { ".results[].createdAt" => "[ignored]", ".results[].updatedAt" => "[ignored]", ".results[].uid" => "[ignored]", ".results[].key" => "[ignored]" }), @r###"
|
meili_snap::snapshot!(meili_snap::json_string!(response, { ".results[].createdAt" => "[ignored]", ".results[].updatedAt" => "[ignored]", ".results[].uid" => "[ignored]", ".results[].key" => "[ignored]" }), @r#"
|
||||||
{
|
{
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
@ -850,6 +850,22 @@ async fn list_api_keys() {
|
|||||||
"createdAt": "[ignored]",
|
"createdAt": "[ignored]",
|
||||||
"updatedAt": "[ignored]"
|
"updatedAt": "[ignored]"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Default Read-Only Admin API Key",
|
||||||
|
"description": "Use it to read information across the whole database. Caution! Do not expose this key on a public frontend",
|
||||||
|
"key": "[ignored]",
|
||||||
|
"uid": "[ignored]",
|
||||||
|
"actions": [
|
||||||
|
"*.get",
|
||||||
|
"keys.get"
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"expiresAt": null,
|
||||||
|
"createdAt": "[ignored]",
|
||||||
|
"updatedAt": "[ignored]"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Default Chat API Key",
|
"name": "Default Chat API Key",
|
||||||
"description": "Use it to chat and search from the frontend",
|
"description": "Use it to chat and search from the frontend",
|
||||||
@ -869,9 +885,9 @@ async fn list_api_keys() {
|
|||||||
],
|
],
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
"limit": 20,
|
"limit": 20,
|
||||||
"total": 4
|
"total": 5
|
||||||
}
|
}
|
||||||
"###);
|
"#);
|
||||||
meili_snap::snapshot!(code, @"200 OK");
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +91,14 @@ async fn create_api_key_bad_actions() {
|
|||||||
// can't parse
|
// can't parse
|
||||||
let (response, code) = server.add_api_key(json!({ "actions": ["doggo"] })).await;
|
let (response, code) = server.add_api_key(json!({ "actions": ["doggo"] })).await;
|
||||||
snapshot!(code, @"400 Bad Request");
|
snapshot!(code, @"400 Bad Request");
|
||||||
snapshot!(json_string!(response), @r###"
|
snapshot!(json_string!(response), @r#"
|
||||||
{
|
{
|
||||||
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `export`, `network.get`, `network.update`, `chatCompletions`, `chats.*`, `chats.get`, `chats.delete`, `chatsSettings.*`, `chatsSettings.get`, `chatsSettings.update`",
|
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `*`, `*.get`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `export`, `network.get`, `network.update`, `chatCompletions`, `chats.*`, `chats.get`, `chats.delete`, `chatsSettings.*`, `chatsSettings.get`, `chatsSettings.update`",
|
||||||
"code": "invalid_api_key_actions",
|
"code": "invalid_api_key_actions",
|
||||||
"type": "invalid_request",
|
"type": "invalid_request",
|
||||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||||
}
|
}
|
||||||
"###);
|
"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -97,6 +97,7 @@ impl Server<Owned> {
|
|||||||
self.use_api_key(master_key);
|
self.use_api_key(master_key);
|
||||||
let (response, code) = self.list_api_keys("").await;
|
let (response, code) = self.list_api_keys("").await;
|
||||||
assert_eq!(200, code, "{:?}", response);
|
assert_eq!(200, code, "{:?}", response);
|
||||||
|
// TODO: relying on the order of keys is not ideal, we should use the name instead
|
||||||
let admin_key = &response["results"][1]["key"];
|
let admin_key = &response["results"][1]["key"];
|
||||||
self.use_api_key(admin_key.as_str().unwrap());
|
self.use_api_key(admin_key.as_str().unwrap());
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use crate::common::{shared_does_not_exists_index, Server};
|
use crate::common::{shared_does_not_exists_index, Server};
|
||||||
|
|
||||||
use crate::json;
|
use crate::json;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
Reference in New Issue
Block a user