2438: Refine keys api r=ManyTheFish a=ManyTheFish

waiting for #2410 and #2444 to be merged.

fix #2369 

Co-authored-by: ManyTheFish <many@meilisearch.com>
This commit is contained in:
bors[bot] 2022-06-02 08:32:35 +00:00 committed by GitHub
commit 08d72e32a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 843 additions and 590 deletions

2
Cargo.lock generated
View File

@ -1973,6 +1973,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
name = "meilisearch-auth" name = "meilisearch-auth"
version = "0.27.1" version = "0.27.1"
dependencies = [ dependencies = [
"base64",
"enum-iterator", "enum-iterator",
"meilisearch-error", "meilisearch-error",
"milli", "milli",
@ -1982,6 +1983,7 @@ dependencies = [
"sha2", "sha2",
"thiserror", "thiserror",
"time 0.3.9", "time 0.3.9",
"uuid",
] ]
[[package]] [[package]]

View File

@ -4,6 +4,7 @@ version = "0.27.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
base64 = "0.13.0"
enum-iterator = "0.7.0" enum-iterator = "0.7.0"
meilisearch-error = { path = "../meilisearch-error" } meilisearch-error = { path = "../meilisearch-error" }
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.28.0" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.28.0" }
@ -13,3 +14,4 @@ serde_json = { version = "1.0.79", features = ["preserve_order"] }
sha2 = "0.10.2" sha2 = "0.10.2"
thiserror = "1.0.30" thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] }
uuid = { version = "0.8.2", features = ["serde", "v4"] }

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
#[repr(u8)] #[repr(u8)]
pub enum Action { pub enum Action {
#[serde(rename = "*")] #[serde(rename = "*")]
All = 0, All = actions::ALL,
#[serde(rename = "search")] #[serde(rename = "search")]
Search = actions::SEARCH, Search = actions::SEARCH,
#[serde(rename = "documents.add")] #[serde(rename = "documents.add")]
@ -36,13 +36,21 @@ pub enum Action {
DumpsGet = actions::DUMPS_GET, DumpsGet = actions::DUMPS_GET,
#[serde(rename = "version")] #[serde(rename = "version")]
Version = actions::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 { impl Action {
pub fn from_repr(repr: u8) -> Option<Self> { pub fn from_repr(repr: u8) -> Option<Self> {
use actions::*; use actions::*;
match repr { match repr {
0 => Some(Self::All), ALL => Some(Self::All),
SEARCH => Some(Self::Search), SEARCH => Some(Self::Search),
DOCUMENTS_ADD => Some(Self::DocumentsAdd), DOCUMENTS_ADD => Some(Self::DocumentsAdd),
DOCUMENTS_GET => Some(Self::DocumentsGet), DOCUMENTS_GET => Some(Self::DocumentsGet),
@ -58,6 +66,10 @@ impl Action {
DUMPS_CREATE => Some(Self::DumpsCreate), DUMPS_CREATE => Some(Self::DumpsCreate),
DUMPS_GET => Some(Self::DumpsGet), DUMPS_GET => Some(Self::DumpsGet),
VERSION => Some(Self::Version), 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, _otherwise => None,
} }
} }
@ -65,7 +77,7 @@ impl Action {
pub fn repr(&self) -> u8 { pub fn repr(&self) -> u8 {
use actions::*; use actions::*;
match self { match self {
Self::All => 0, Self::All => ALL,
Self::Search => SEARCH, Self::Search => SEARCH,
Self::DocumentsAdd => DOCUMENTS_ADD, Self::DocumentsAdd => DOCUMENTS_ADD,
Self::DocumentsGet => DOCUMENTS_GET, Self::DocumentsGet => DOCUMENTS_GET,
@ -81,11 +93,16 @@ impl Action {
Self::DumpsCreate => DUMPS_CREATE, Self::DumpsCreate => DUMPS_CREATE,
Self::DumpsGet => DUMPS_GET, Self::DumpsGet => DUMPS_GET,
Self::Version => VERSION, Self::Version => VERSION,
Self::KeysAdd => KEYS_CREATE,
Self::KeysGet => KEYS_GET,
Self::KeysUpdate => KEYS_UPDATE,
Self::KeysDelete => KEYS_DELETE,
} }
} }
} }
pub mod actions { pub mod actions {
pub(crate) const ALL: u8 = 0;
pub const SEARCH: u8 = 1; pub const SEARCH: u8 = 1;
pub const DOCUMENTS_ADD: u8 = 2; pub const DOCUMENTS_ADD: u8 = 2;
pub const DOCUMENTS_GET: u8 = 3; pub const DOCUMENTS_GET: u8 = 3;
@ -101,4 +118,8 @@ pub mod actions {
pub const DUMPS_CREATE: u8 = 13; pub const DUMPS_CREATE: u8 = 13;
pub const DUMPS_GET: u8 = 14; pub const DUMPS_GET: u8 = 14;
pub const VERSION: u8 = 15; 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;
} }

View File

@ -1,5 +1,6 @@
use serde_json::Deserializer;
use std::fs::File; use std::fs::File;
use std::io::BufRead;
use std::io::BufReader; use std::io::BufReader;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
@ -36,10 +37,9 @@ impl AuthController {
return Ok(()); return Ok(());
} }
let mut reader = BufReader::new(File::open(&keys_file_path)?).lines(); let reader = BufReader::new(File::open(&keys_file_path)?);
while let Some(key) = reader.next().transpose()? { for key in Deserializer::from_reader(reader).into_iter() {
let key = serde_json::from_str(&key)?; store.put_api_key(key?)?;
store.put_api_key(key)?;
} }
Ok(()) Ok(())

View File

@ -18,8 +18,18 @@ pub enum AuthControllerError {
InvalidApiKeyExpiresAt(Value), InvalidApiKeyExpiresAt(Value),
#[error("`description` field value `{0}` is invalid. It should be a string or specified as a null value.")] #[error("`description` field value `{0}` is invalid. It should be a string or specified as a null value.")]
InvalidApiKeyDescription(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 UUID v4 string or omitted.")]
InvalidApiKeyUid(Value),
#[error("API key `{0}` not found.")] #[error("API key `{0}` not found.")]
ApiKeyNotFound(String), ApiKeyNotFound(String),
#[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}")] #[error("Internal error: {0}")]
Internal(Box<dyn Error + Send + Sync + 'static>), Internal(Box<dyn Error + Send + Sync + 'static>),
} }
@ -39,7 +49,11 @@ impl ErrorCode for AuthControllerError {
Self::InvalidApiKeyIndexes(_) => Code::InvalidApiKeyIndexes, Self::InvalidApiKeyIndexes(_) => Code::InvalidApiKeyIndexes,
Self::InvalidApiKeyExpiresAt(_) => Code::InvalidApiKeyExpiresAt, Self::InvalidApiKeyExpiresAt(_) => Code::InvalidApiKeyExpiresAt,
Self::InvalidApiKeyDescription(_) => Code::InvalidApiKeyDescription, Self::InvalidApiKeyDescription(_) => Code::InvalidApiKeyDescription,
Self::InvalidApiKeyName(_) => Code::InvalidApiKeyName,
Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound, Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound,
Self::InvalidApiKeyUid(_) => Code::InvalidApiKeyUid,
Self::ApiKeyAlreadyExists(_) => Code::ApiKeyAlreadyExists,
Self::ImmutableField(_) => Code::ImmutableField,
Self::Internal(_) => Code::Internal, Self::Internal(_) => Code::Internal,
} }
} }

View File

@ -1,18 +1,21 @@
use crate::action::Action; use crate::action::Action;
use crate::error::{AuthControllerError, Result}; use crate::error::{AuthControllerError, Result};
use crate::store::{KeyId, KEY_ID_LENGTH}; use crate::store::KeyId;
use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{from_value, Value}; use serde_json::{from_value, Value};
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 uuid::Uuid;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Key { pub struct Key {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
pub id: KeyId, #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub uid: KeyId,
pub actions: Vec<Action>, pub actions: Vec<Action>,
pub indexes: Vec<String>, pub indexes: Vec<String>,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
@ -25,16 +28,27 @@ pub struct Key {
impl Key { impl Key {
pub fn create_from_value(value: Value) -> Result<Self> { pub fn create_from_value(value: Value) -> Result<Self> {
let description = match value.get("description") { let name = match value.get("name") {
Some(Value::Null) => None, None | Some(Value::Null) => None,
Some(des) => Some( Some(des) => from_value(des.clone())
from_value(des.clone()) .map(Some)
.map_err(|_| AuthControllerError::InvalidApiKeyDescription(des.clone()))?, .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?,
),
None => None,
}; };
let id = generate_id(); let description = match value.get("description") {
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(
|| Ok(Uuid::new_v4()),
|uid| {
from_value(uid.clone())
.map_err(|_| AuthControllerError::InvalidApiKeyUid(uid.clone()))
},
)?;
let actions = value let actions = value
.get("actions") .get("actions")
@ -61,8 +75,9 @@ impl Key {
let updated_at = created_at; let updated_at = created_at;
Ok(Self { Ok(Self {
name,
description, description,
id, uid,
actions, actions,
indexes, indexes,
expires_at, expires_at,
@ -78,20 +93,34 @@ impl Key {
self.description = des?; self.description = des?;
} }
if let Some(act) = value.get("actions") { if let Some(des) = value.get("name") {
let act = from_value(act.clone()) let des = from_value(des.clone())
.map_err(|_| AuthControllerError::InvalidApiKeyActions(act.clone())); .map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()));
self.actions = act?; self.name = des?;
} }
if let Some(ind) = value.get("indexes") { if value.get("uid").is_some() {
let ind = from_value(ind.clone()) return Err(AuthControllerError::ImmutableField("uid".to_string()));
.map_err(|_| AuthControllerError::InvalidApiKeyIndexes(ind.clone()));
self.indexes = ind?;
} }
if let Some(exp) = value.get("expiresAt") { if value.get("actions").is_some() {
self.expires_at = parse_expiration_date(exp)?; 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(); self.updated_at = OffsetDateTime::now_utc();
@ -101,9 +130,11 @@ impl Key {
pub(crate) fn default_admin() -> Self { pub(crate) fn default_admin() -> Self {
let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc();
let uid = Uuid::new_v4();
Self { Self {
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()),
id: generate_id(), 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], actions: vec![Action::All],
indexes: vec!["*".to_string()], indexes: vec!["*".to_string()],
expires_at: None, expires_at: None,
@ -114,11 +145,11 @@ impl Key {
pub(crate) fn default_search() -> Self { pub(crate) fn default_search() -> Self {
let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc();
let uid = Uuid::new_v4();
Self { Self {
description: Some( name: Some("Default Search API Key".to_string()),
"Default Search API Key (Use it to search from the frontend)".to_string(), description: Some("Use it to search from the frontend".to_string()),
), uid,
id: generate_id(),
actions: vec![Action::Search], actions: vec![Action::Search],
indexes: vec!["*".to_string()], indexes: vec!["*".to_string()],
expires_at: None, expires_at: None,
@ -128,19 +159,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<Option<OffsetDateTime>> { fn parse_expiration_date(value: &Value) -> Result<Option<OffsetDateTime>> {
match value { match value {
Value::String(string) => OffsetDateTime::parse(string, &Rfc3339) Value::String(string) => OffsetDateTime::parse(string, &Rfc3339)

View File

@ -6,17 +6,17 @@ mod store;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use std::str::from_utf8;
use std::sync::Arc; use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid;
pub use action::{actions, Action}; pub use action::{actions, Action};
use error::{AuthControllerError, Result}; use error::{AuthControllerError, Result};
pub use key::Key; pub use key::Key;
use store::generate_key_as_base64;
pub use store::open_auth_store_env; pub use store::open_auth_store_env;
use store::HeedAuthStore; use store::HeedAuthStore;
@ -42,62 +42,75 @@ impl AuthController {
pub fn create_key(&self, value: Value) -> Result<Key> { pub fn create_key(&self, value: Value) -> Result<Key> {
let key = Key::create_from_value(value)?; 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<str>, value: Value) -> Result<Key> { pub fn update_key(&self, uid: Uuid, value: Value) -> Result<Key> {
let mut key = self.get_key(key)?; let mut key = self.get_key(uid)?;
key.update_from_value(value)?; key.update_from_value(value)?;
self.store.put_api_key(key) self.store.put_api_key(key)
} }
pub fn get_key(&self, key: impl AsRef<str>) -> Result<Key> { pub fn get_key(&self, uid: Uuid) -> Result<Key> {
self.store self.store
.get_api_key(&key)? .get_api_key(uid)?
.ok_or_else(|| AuthControllerError::ApiKeyNotFound(key.as_ref().to_string())) .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))
}
pub fn get_optional_uid_from_encoded_key(&self, encoded_key: &[u8]) -> Result<Option<Uuid>> {
match &self.master_key {
Some(master_key) => self
.store
.get_uid_from_encoded_key(encoded_key, master_key.as_bytes()),
None => Ok(None),
}
}
pub fn get_uid_from_encoded_key(&self, encoded_key: &str) -> Result<Uuid> {
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( pub fn get_key_filters(
&self, &self,
key: impl AsRef<str>, uid: Uuid,
search_rules: Option<SearchRules>, search_rules: Option<SearchRules>,
) -> Result<AuthFilter> { ) -> Result<AuthFilter> {
let mut filters = AuthFilter::default(); let mut filters = AuthFilter::default();
if self let key = self
.master_key .store
.as_ref() .get_api_key(uid)?
.map_or(false, |master_key| master_key != key.as_ref()) .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?;
{
let key = self
.store
.get_api_key(&key)?
.ok_or_else(|| AuthControllerError::ApiKeyNotFound(key.as_ref().to_string()))?;
if !key.indexes.iter().any(|i| i.as_str() == "*") { if !key.indexes.iter().any(|i| i.as_str() == "*") {
filters.search_rules = match search_rules { filters.search_rules = match search_rules {
// Intersect search_rules with parent key authorized indexes. // Intersect search_rules with parent key authorized indexes.
Some(search_rules) => SearchRules::Map( Some(search_rules) => SearchRules::Map(
key.indexes key.indexes
.into_iter() .into_iter()
.filter_map(|index| { .filter_map(|index| {
search_rules search_rules
.get_index_search_rules(&index) .get_index_search_rules(&index)
.map(|index_search_rules| (index, Some(index_search_rules))) .map(|index_search_rules| (index, Some(index_search_rules)))
}) })
.collect(), .collect(),
), ),
None => SearchRules::Set(key.indexes.into_iter().collect()), None => SearchRules::Set(key.indexes.into_iter().collect()),
}; };
} else if let Some(search_rules) = search_rules { } else if let Some(search_rules) = search_rules {
filters.search_rules = search_rules; filters.search_rules = search_rules;
}
filters.allow_index_creation = key
.actions
.iter()
.any(|&action| action == Action::IndexesAdd || action == Action::All);
} }
filters.allow_index_creation = key
.actions
.iter()
.any(|&action| action == Action::IndexesAdd || action == Action::All);
Ok(filters) Ok(filters)
} }
@ -105,13 +118,11 @@ impl AuthController {
self.store.list_api_keys() self.store.list_api_keys()
} }
pub fn delete_key(&self, key: impl AsRef<str>) -> Result<()> { pub fn delete_key(&self, uid: Uuid) -> Result<()> {
if self.store.delete_api_key(&key)? { if self.store.delete_api_key(uid)? {
Ok(()) Ok(())
} else { } else {
Err(AuthControllerError::ApiKeyNotFound( Err(AuthControllerError::ApiKeyNotFound(uid.to_string()))
key.as_ref().to_string(),
))
} }
} }
@ -121,32 +132,32 @@ impl AuthController {
/// Generate a valid key from a key id using the current master key. /// Generate a valid key from a key id using the current master key.
/// Returns None if no master key has been set. /// Returns None if no master key has been set.
pub fn generate_key(&self, id: &str) -> Option<String> { pub fn generate_key(&self, uid: Uuid) -> Option<String> {
self.master_key self.master_key
.as_ref() .as_ref()
.map(|master_key| generate_key(master_key.as_bytes(), id)) .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 /// Check if the provided key is authorized to make a specific action
/// without checking if the key is valid. /// without checking if the key is valid.
pub fn is_key_authorized( pub fn is_key_authorized(
&self, &self,
key: &[u8], uid: Uuid,
action: Action, action: Action,
index: Option<&str>, index: Option<&str>,
) -> Result<bool> { ) -> Result<bool> {
match self match self
.store .store
// check if the key has access to all indexes. // check if the key has access to all indexes.
.get_expiration_date(key, action, None)? .get_expiration_date(uid, action, None)?
.or(match index { .or(match index {
// else check if the key has access to the requested index. // else check if the key has access to the requested index.
Some(index) => { Some(index) => {
self.store 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. // 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. // check expiration date.
Some(Some(exp)) => Ok(OffsetDateTime::now_utc() < exp), Some(Some(exp)) => Ok(OffsetDateTime::now_utc() < exp),
@ -156,29 +167,6 @@ impl AuthController {
None => Ok(false), 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<bool> {
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<bool> {
if self.is_key_authorized(key, action, index)? {
self.is_key_valid(key)
} else {
Ok(false)
}
}
} }
pub struct AuthFilter { pub struct AuthFilter {
@ -258,12 +246,6 @@ pub struct IndexSearchRules {
pub filter: Option<serde_json::Value>, pub filter: Option<serde_json::Value>,
} }
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<()> { fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
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())?;

View File

@ -1,4 +1,3 @@
use enum_iterator::IntoEnumIterator;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -8,20 +7,22 @@ use std::path::Path;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use enum_iterator::IntoEnumIterator;
use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson};
use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; use milli::heed::{Database, Env, EnvOpenOptions, RwTxn};
use sha2::{Digest, Sha256};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid;
use super::error::Result; use super::error::Result;
use super::{Action, Key}; use super::{Action, Key};
const AUTH_STORE_SIZE: usize = 1_073_741_824; //1GiB const AUTH_STORE_SIZE: usize = 1_073_741_824; //1GiB
pub const KEY_ID_LENGTH: usize = 8;
const AUTH_DB_PATH: &str = "auth"; const AUTH_DB_PATH: &str = "auth";
const KEY_DB_NAME: &str = "api-keys"; const KEY_DB_NAME: &str = "api-keys";
const KEY_ID_ACTION_INDEX_EXPIRATION_DB_NAME: &str = "keyid-action-index-expiration"; 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)] #[derive(Clone)]
pub struct HeedAuthStore { pub struct HeedAuthStore {
@ -73,12 +74,13 @@ impl HeedAuthStore {
} }
pub fn put_api_key(&self, key: Key) -> Result<Key> { pub fn put_api_key(&self, key: Key) -> Result<Key> {
let uid = key.uid;
let mut wtxn = self.env.write_txn()?; 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. // 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. // create inverted database.
let db = self.action_keyid_index_expiration; let db = self.action_keyid_index_expiration;
@ -93,13 +95,13 @@ impl HeedAuthStore {
for action in actions { for action in actions {
if no_index_restriction { if no_index_restriction {
// If there is no index restriction we put None. // 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 {
// else we create a key for each index. // else we create a key for each index.
for index in key.indexes.iter() { for index in key.indexes.iter() {
db.put( db.put(
&mut wtxn, &mut wtxn,
&(&id, &action, Some(index.as_bytes())), &(&uid, &action, Some(index.as_bytes())),
&key.expires_at, &key.expires_at,
)?; )?;
} }
@ -111,24 +113,39 @@ impl HeedAuthStore {
Ok(key) Ok(key)
} }
pub fn get_api_key(&self, key: impl AsRef<str>) -> Result<Option<Key>> { pub fn get_api_key(&self, uid: Uuid) -> Result<Option<Key>> {
let rtxn = self.env.read_txn()?; let rtxn = self.env.read_txn()?;
match self.get_key_id(key.as_ref().as_bytes()) { self.keys.get(&rtxn, uid.as_bytes()).map_err(|e| e.into())
Some(id) => self.keys.get(&rtxn, &id).map_err(|e| e.into()),
None => Ok(None),
}
} }
pub fn delete_api_key(&self, key: impl AsRef<str>) -> Result<bool> { pub fn get_uid_from_encoded_key(
&self,
encoded_key: &[u8],
master_key: &[u8],
) -> Result<Option<Uuid>> {
let rtxn = self.env.read_txn()?;
let uid = self
.keys
.remap_data_type::<DecodeIgnore>()
.iter(&rtxn)?
.filter_map(|res| match res {
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))
}
_ => None,
})
.next();
Ok(uid)
}
pub fn delete_api_key(&self, uid: Uuid) -> Result<bool> {
let mut wtxn = self.env.write_txn()?; let mut wtxn = self.env.write_txn()?;
let existing = match self.get_key_id(key.as_ref().as_bytes()) { let existing = self.keys.delete(&mut wtxn, uid.as_bytes())?;
Some(id) => { self.delete_key_from_inverted_db(&mut wtxn, &uid)?;
let existing = self.keys.delete(&mut wtxn, &id)?;
self.delete_key_from_inverted_db(&mut wtxn, &id)?;
existing
}
None => false,
};
wtxn.commit()?; wtxn.commit()?;
Ok(existing) Ok(existing)
@ -147,49 +164,37 @@ impl HeedAuthStore {
pub fn get_expiration_date( pub fn get_expiration_date(
&self, &self,
key: &[u8], uid: Uuid,
action: Action, action: Action,
index: Option<&[u8]>, index: Option<&[u8]>,
) -> Result<Option<Option<OffsetDateTime>>> { ) -> Result<Option<Option<OffsetDateTime>>> {
let rtxn = self.env.read_txn()?; let rtxn = self.env.read_txn()?;
match self.get_key_id(key) { let tuple = (&uid, &action, index);
Some(id) => { Ok(self.action_keyid_index_expiration.get(&rtxn, &tuple)?)
let tuple = (&id, &action, index);
Ok(self.action_keyid_index_expiration.get(&rtxn, &tuple)?)
}
None => Ok(None),
}
} }
pub fn prefix_first_expiration_date( pub fn prefix_first_expiration_date(
&self, &self,
key: &[u8], uid: Uuid,
action: Action, action: Action,
) -> Result<Option<Option<OffsetDateTime>>> { ) -> Result<Option<Option<OffsetDateTime>>> {
let rtxn = self.env.read_txn()?; let rtxn = self.env.read_txn()?;
match self.get_key_id(key) { let tuple = (&uid, &action, None);
Some(id) => { let exp = self
let tuple = (&id, &action, None); .action_keyid_index_expiration
Ok(self .prefix_iter(&rtxn, &tuple)?
.action_keyid_index_expiration .next()
.prefix_iter(&rtxn, &tuple)? .transpose()?
.next() .map(|(_, expiration)| expiration);
.transpose()?
.map(|(_, expiration)| expiration))
}
None => Ok(None),
}
}
pub fn get_key_id(&self, key: &[u8]) -> Option<KeyId> { Ok(exp)
try_split_array_at::<_, KEY_ID_LENGTH>(key).map(|(id, _)| *id)
} }
fn delete_key_from_inverted_db(&self, wtxn: &mut RwTxn, key: &KeyId) -> Result<()> { fn delete_key_from_inverted_db(&self, wtxn: &mut RwTxn, key: &KeyId) -> Result<()> {
let mut iter = self let mut iter = self
.action_keyid_index_expiration .action_keyid_index_expiration
.remap_types::<ByteSlice, DecodeIgnore>() .remap_types::<ByteSlice, DecodeIgnore>()
.prefix_iter_mut(wtxn, key)?; .prefix_iter_mut(wtxn, key.as_bytes())?;
while iter.next().transpose()?.is_some() { while iter.next().transpose()?.is_some() {
// safety: we don't keep references from inside the LMDB database. // safety: we don't keep references from inside the LMDB database.
unsafe { iter.del_current()? }; unsafe { iter.del_current()? };
@ -207,14 +212,15 @@ impl<'a> milli::heed::BytesDecode<'a> for KeyIdActionCodec {
type DItem = (KeyId, Action, Option<&'a [u8]>); type DItem = (KeyId, Action, Option<&'a [u8]>);
fn bytes_decode(bytes: &'a [u8]) -> Option<Self::DItem> { fn bytes_decode(bytes: &'a [u8]) -> Option<Self::DItem> {
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)? { let (action_bytes, index) = match try_split_array_at(action_bytes)? {
(action, []) => (action, None), (action, []) => (action, None),
(action, index) => (action, Some(index)), (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))?; let action = Action::from_repr(u8::from_be_bytes(*action_bytes))?;
Some((*key_id, action, index)) Some((key_id, action, index))
} }
} }
@ -224,7 +230,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
fn bytes_encode((key_id, action, index): &Self::EItem) -> Option<Cow<[u8]>> { fn bytes_encode((key_id, action, index): &Self::EItem) -> Option<Cow<[u8]>> {
let mut bytes = Vec::new(); 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()); let action_bytes = u8::to_be_bytes(action.repr());
bytes.extend_from_slice(&action_bytes); bytes.extend_from_slice(&action_bytes);
if let Some(index) = index { if let Some(index) = index {
@ -235,6 +241,12 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
} }
} }
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)
}
/// Divides one slice into two at an index, returns `None` if mid is out of bounds. /// Divides one slice into two at an index, returns `None` if mid is out of bounds.
pub fn try_split_at<T>(slice: &[T], mid: usize) -> Option<(&[T], &[T])> { pub fn try_split_at<T>(slice: &[T], mid: usize) -> Option<(&[T], &[T])> {
if mid <= slice.len() { if mid <= slice.len() {

View File

@ -166,6 +166,10 @@ pub enum Code {
InvalidApiKeyIndexes, InvalidApiKeyIndexes,
InvalidApiKeyExpiresAt, InvalidApiKeyExpiresAt,
InvalidApiKeyDescription, InvalidApiKeyDescription,
InvalidApiKeyName,
InvalidApiKeyUid,
ImmutableField,
ApiKeyAlreadyExists,
} }
impl Code { impl Code {
@ -272,6 +276,10 @@ impl Code {
InvalidApiKeyDescription => { InvalidApiKeyDescription => {
ErrCode::invalid("invalid_api_key_description", StatusCode::BAD_REQUEST) 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),
ImmutableField => ErrCode::invalid("immutable_field", StatusCode::BAD_REQUEST),
InvalidMinWordLengthForTypo => { InvalidMinWordLengthForTypo => {
ErrCode::invalid("invalid_min_word_length_for_typo", StatusCode::BAD_REQUEST) ErrCode::invalid("invalid_min_word_length_for_typo", StatusCode::BAD_REQUEST)
} }

View File

@ -75,7 +75,7 @@ thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] }
tokio = { version = "1.17.0", features = ["full"] } tokio = { version = "1.17.0", features = ["full"] }
tokio-stream = "0.1.8" tokio-stream = "0.1.8"
uuid = { version = "0.8.2", features = ["serde"] } uuid = { version = "0.8.2", features = ["serde", "v4"] }
walkdir = "2.3.2" walkdir = "2.3.2"
[dev-dependencies] [dev-dependencies]

View File

@ -132,6 +132,7 @@ pub mod policies {
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid;
use crate::extractors::authentication::Policy; use crate::extractors::authentication::Policy;
use meilisearch_auth::{Action, AuthController, AuthFilter, SearchRules}; use meilisearch_auth::{Action, AuthController, AuthFilter, SearchRules};
@ -146,34 +147,21 @@ pub mod policies {
validation validation
} }
/// Extracts the key prefix 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_prefix(token: &str) -> Option<String> { fn extract_key_id(token: &str) -> Option<Uuid> {
let mut validation = tenant_token_validation(); let mut validation = tenant_token_validation();
validation.insecure_disable_signature_validation(); validation.insecure_disable_signature_validation();
let dummy_key = DecodingKey::from_secret(b"secret"); let dummy_key = DecodingKey::from_secret(b"secret");
let token_data = decode::<Claims>(token, &dummy_key, &validation).ok()?; let token_data = decode::<Claims>(token, &dummy_key, &validation).ok()?;
// get token fields without validating it. // get token fields without validating it.
let Claims { api_key_prefix, .. } = token_data.claims; let Claims { api_key_uid, .. } = token_data.claims;
Some(api_key_prefix) Some(api_key_uid)
} }
pub struct MasterPolicy; fn is_keys_action(action: u8) -> bool {
use actions::*;
impl Policy for MasterPolicy { matches!(action, KEYS_GET | KEYS_CREATE | KEYS_UPDATE | KEYS_DELETE)
fn authenticate(
auth: AuthController,
token: &str,
_index: Option<&str>,
) -> Option<AuthFilter> {
if let Some(master_key) = auth.get_master_key() {
if master_key == token {
return Some(AuthFilter::default());
}
}
None
}
} }
pub struct ActionPolicy<const A: u8>; pub struct ActionPolicy<const A: u8>;
@ -185,7 +173,12 @@ pub mod policies {
index: Option<&str>, index: Option<&str>,
) -> Option<AuthFilter> { ) -> Option<AuthFilter> {
// authenticate if token is the master key. // 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)
{
return Some(AuthFilter::default()); return Some(AuthFilter::default());
} }
@ -195,8 +188,10 @@ pub mod policies {
return Some(filters); return Some(filters);
} else if let Some(action) = Action::from_repr(A) { } else if let Some(action) = Action::from_repr(A) {
// API key // API key
if let Ok(true) = auth.authenticate(token.as_bytes(), action, index) { if let Ok(Some(uid)) = auth.get_optional_uid_from_encoded_key(token.as_bytes()) {
return auth.get_key_filters(token, None).ok(); if let Ok(true) = auth.is_key_authorized(uid, action, index) {
return auth.get_key_filters(uid, None).ok();
}
} }
} }
@ -215,14 +210,11 @@ pub mod policies {
return None; 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. // check if parent key is authorized to do the action.
if auth if auth.is_key_authorized(uid, Action::Search, index).ok()? {
.is_key_authorized(api_key_prefix.as_bytes(), Action::Search, index)
.ok()?
{
// Check if tenant token is valid. // Check if tenant token is valid.
let key = auth.generate_key(&api_key_prefix)?; let key = auth.generate_key(uid)?;
let data = decode::<Claims>( let data = decode::<Claims>(
token, token,
&DecodingKey::from_secret(key.as_bytes()), &DecodingKey::from_secret(key.as_bytes()),
@ -245,7 +237,7 @@ pub mod policies {
} }
return auth return auth
.get_key_filters(api_key_prefix, Some(data.claims.search_rules)) .get_key_filters(uid, Some(data.claims.search_rules))
.ok(); .ok();
} }
@ -258,6 +250,6 @@ pub mod policies {
struct Claims { struct Claims {
search_rules: SearchRules, search_rules: SearchRules,
exp: Option<i64>, exp: Option<i64>,
api_key_prefix: String, api_key_uid: Uuid,
} }
} }

View File

@ -1,4 +1,5 @@
use std::str; use std::str;
use uuid::Uuid;
use actix_web::{web, HttpRequest, HttpResponse}; 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))), .route(web::get().to(SeqHandler(list_api_keys))),
) )
.service( .service(
web::resource("/{api_key}") web::resource("/{key}")
.route(web::get().to(SeqHandler(get_api_key))) .route(web::get().to(SeqHandler(get_api_key)))
.route(web::patch().to(SeqHandler(patch_api_key))) .route(web::patch().to(SeqHandler(patch_api_key)))
.route(web::delete().to(SeqHandler(delete_api_key))), .route(web::delete().to(SeqHandler(delete_api_key))),
@ -28,7 +29,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
} }
pub async fn create_api_key( pub async fn create_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>,
body: web::Json<Value>, body: web::Json<Value>,
_req: HttpRequest, _req: HttpRequest,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -44,7 +45,7 @@ pub async fn create_api_key(
} }
pub async fn list_api_keys( pub async fn list_api_keys(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>,
_req: HttpRequest, _req: HttpRequest,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
@ -62,12 +63,16 @@ pub async fn list_api_keys(
} }
pub async fn get_api_key( pub async fn get_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let api_key = path.into_inner().api_key; let key = path.into_inner().key;
let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { 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.get_uid_from_encoded_key(&key))?;
let key = auth_controller.get_key(uid)?;
Ok(KeyView::from_key(key, &auth_controller)) Ok(KeyView::from_key(key, &auth_controller))
}) })
.await .await
@ -77,14 +82,17 @@ pub async fn get_api_key(
} }
pub async fn patch_api_key( pub async fn patch_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>,
body: web::Json<Value>, body: web::Json<Value>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let api_key = path.into_inner().api_key; let key = path.into_inner().key;
let body = body.into_inner(); let body = body.into_inner();
let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { 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.get_uid_from_encoded_key(&key))?;
let key = auth_controller.update_key(uid, body)?;
Ok(KeyView::from_key(key, &auth_controller)) Ok(KeyView::from_key(key, &auth_controller))
}) })
.await .await
@ -94,27 +102,33 @@ pub async fn patch_api_key(
} }
pub async fn delete_api_key( pub async fn delete_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, AuthController>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let api_key = path.into_inner().api_key; let key = path.into_inner().key;
tokio::task::spawn_blocking(move || auth_controller.delete_key(&api_key)) tokio::task::spawn_blocking(move || {
.await let uid =
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??; Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
auth_controller.delete_key(uid)
})
.await
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct AuthParam { pub struct AuthParam {
api_key: String, key: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct KeyView { struct KeyView {
name: Option<String>,
description: Option<String>, description: Option<String>,
key: String, key: String,
uid: Uuid,
actions: Vec<Action>, actions: Vec<Action>,
indexes: Vec<String>, indexes: Vec<String>,
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")] #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
@ -127,12 +141,13 @@ struct KeyView {
impl KeyView { impl KeyView {
fn from_key(key: Key, auth: &AuthController) -> Self { fn from_key(key: Key, auth: &AuthController) -> Self {
let key_id = str::from_utf8(&key.id).unwrap(); let generated_key = auth.generate_key(key.uid).unwrap_or_default();
let generated_key = auth.generate_key(key_id).unwrap_or_default();
KeyView { KeyView {
name: key.name,
description: key.description, description: key.description,
key: generated_key, key: generated_key,
uid: key.uid,
actions: key.actions, actions: key.actions,
indexes: key.indexes, indexes: key.indexes,
expires_at: key.expires_at, expires_at: key.expires_at,

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,11 @@ pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'
("GET", "/stats") => hashset!{"stats.get", "*"}, ("GET", "/stats") => hashset!{"stats.get", "*"},
("POST", "/dumps") => hashset!{"dumps.create", "*"}, ("POST", "/dumps") => hashset!{"dumps.create", "*"},
("GET", "/version") => hashset!{"version", "*"}, ("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; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
@ -92,8 +97,14 @@ async fn error_access_expired_key() {
for (method, route) in AUTHORIZATIONS.keys() { for (method, route) in AUTHORIZATIONS.keys() {
let (response, code) = server.dummy_request(method, route).await; let (response, code) = server.dummy_request(method, route).await;
assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(
assert_eq!(code, 403); 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; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); 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; let (response, code) = server.dummy_request(method, route).await;
assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(
assert_eq!(code, 403); response,
INVALID_RESPONSE.clone(),
"on route: {:?} - {:?}",
method,
route
);
assert_eq!(403, code, "{:?}", &response);
} }
} }
@ -132,36 +149,54 @@ async fn error_access_unauthorized_index() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn error_access_unauthorized_action() { async fn error_access_unauthorized_action() {
let mut server = Server::new_auth().await; 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), action) in AUTHORIZATIONS.iter() { for ((method, route), action) in AUTHORIZATIONS.iter() {
// create a new API key letting only the needed action.
server.use_api_key("MASTER_KEY"); server.use_api_key("MASTER_KEY");
// Patch API key letting all rights but the needed one.
let content = json!({ let content = json!({
"indexes": ["products"],
"actions": ALL_ACTIONS.difference(action).collect::<Vec<_>>(), "actions": ALL_ACTIONS.difference(action).collect::<Vec<_>>(),
"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); server.use_api_key(&key);
let (response, code) = server.dummy_request(method, route).await; let (response, code) = server.dummy_request(method, route).await;
assert_eq!(response, INVALID_RESPONSE.clone()); assert_eq!(
assert_eq!(code, 403); 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 have access to all routes.
for ((method, route), _) in AUTHORIZATIONS.iter() {
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 +204,34 @@ async fn error_access_unauthorized_action() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn access_authorized_restricted_index() { async fn access_authorized_restricted_index() {
let mut server = Server::new_auth().await; 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 ((method, route), actions) in AUTHORIZATIONS.iter() {
for action in actions { 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!({ let content = json!({
"indexes": ["products"],
"actions": [action], "actions": [action],
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
server.use_api_key("MASTER_KEY"); let (response, code) = server.add_api_key(content).await;
let (_, code) = server.patch_api_key(&key, content).await; assert_eq!(201, code, "{:?}", &response);
assert_eq!(code, 200); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap();
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.dummy_request(method, route).await; 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); assert_ne!(code, 403);
} }
} }
@ -208,36 +241,35 @@ async fn access_authorized_restricted_index() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn access_authorized_no_index_restriction() { async fn access_authorized_no_index_restriction() {
let mut server = Server::new_auth().await; 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 ((method, route), actions) in AUTHORIZATIONS.iter() {
for action in actions { for action in actions {
// create a new API key letting only the needed action.
server.use_api_key("MASTER_KEY"); server.use_api_key("MASTER_KEY");
// Patch API key letting only the needed action.
let content = json!({ let content = json!({
"indexes": ["products"],
"actions": [action], "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); server.use_api_key(&key);
let (response, code) = server.dummy_request(method, route).await; 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); assert_ne!(code, 403);
} }
} }
@ -247,16 +279,16 @@ async fn access_authorized_no_index_restriction() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn access_authorized_stats_restricted_index() { async fn access_authorized_stats_restricted_index() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on `products` index only. // create key with access on `products` index only.
@ -266,7 +298,7 @@ async fn access_authorized_stats_restricted_index() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -274,7 +306,7 @@ async fn access_authorized_stats_restricted_index() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.stats().await; let (response, code) = server.stats().await;
assert_eq!(code, 200); assert_eq!(200, code, "{:?}", &response);
// key should have access on `products` index. // key should have access on `products` index.
assert!(response["indexes"].get("products").is_some()); assert!(response["indexes"].get("products").is_some());
@ -287,16 +319,16 @@ async fn access_authorized_stats_restricted_index() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn access_authorized_stats_no_index_restriction() { async fn access_authorized_stats_no_index_restriction() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on all indexes. // create key with access on all indexes.
@ -306,7 +338,7 @@ async fn access_authorized_stats_no_index_restriction() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -314,7 +346,7 @@ async fn access_authorized_stats_no_index_restriction() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.stats().await; let (response, code) = server.stats().await;
assert_eq!(code, 200); assert_eq!(200, code, "{:?}", &response);
// key should have access on `products` index. // key should have access on `products` index.
assert!(response["indexes"].get("products").is_some()); assert!(response["indexes"].get("products").is_some());
@ -327,16 +359,16 @@ async fn access_authorized_stats_no_index_restriction() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn list_authorized_indexes_restricted_index() { async fn list_authorized_indexes_restricted_index() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on `products` index only. // create key with access on `products` index only.
@ -346,7 +378,7 @@ async fn list_authorized_indexes_restricted_index() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -354,7 +386,7 @@ async fn list_authorized_indexes_restricted_index() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.list_indexes(None, None).await; 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(); let response = response["results"].as_array().unwrap();
// key should have access on `products` index. // key should have access on `products` index.
@ -368,16 +400,16 @@ async fn list_authorized_indexes_restricted_index() {
#[cfg_attr(target_os = "windows", ignore)] #[cfg_attr(target_os = "windows", ignore)]
async fn list_authorized_indexes_no_index_restriction() { async fn list_authorized_indexes_no_index_restriction() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on all indexes. // create key with access on all indexes.
@ -387,7 +419,7 @@ async fn list_authorized_indexes_no_index_restriction() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -395,7 +427,7 @@ async fn list_authorized_indexes_no_index_restriction() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.list_indexes(None, None).await; 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(); let response = response["results"].as_array().unwrap();
// key should have access on `products` index. // key should have access on `products` index.
@ -408,16 +440,16 @@ async fn list_authorized_indexes_no_index_restriction() {
#[actix_rt::test] #[actix_rt::test]
async fn list_authorized_tasks_restricted_index() { async fn list_authorized_tasks_restricted_index() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on `products` index only. // create key with access on `products` index only.
@ -427,7 +459,7 @@ async fn list_authorized_tasks_restricted_index() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -435,7 +467,7 @@ async fn list_authorized_tasks_restricted_index() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.service.get("/tasks").await; let (response, code) = server.service.get("/tasks").await;
assert_eq!(code, 200); assert_eq!(200, code, "{:?}", &response);
println!("{}", response); println!("{}", response);
let response = response["results"].as_array().unwrap(); let response = response["results"].as_array().unwrap();
// key should have access on `products` index. // key should have access on `products` index.
@ -448,16 +480,16 @@ async fn list_authorized_tasks_restricted_index() {
#[actix_rt::test] #[actix_rt::test]
async fn list_authorized_tasks_no_index_restriction() { async fn list_authorized_tasks_no_index_restriction() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY"); server.use_admin_key("MASTER_KEY").await;
// create index `test` // create index `test`
let index = server.index("test"); let index = server.index("test");
let (_, code) = index.create(Some("id")).await; let (response, code) = index.create(Some("id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
// create index `products` // create index `products`
let index = server.index("products"); let index = server.index("products");
let (_, code) = index.create(Some("product_id")).await; let (response, code) = index.create(Some("product_id")).await;
assert_eq!(code, 202); assert_eq!(202, code, "{:?}", &response);
index.wait_task(0).await; index.wait_task(0).await;
// create key with access on all indexes. // create key with access on all indexes.
@ -467,7 +499,7 @@ async fn list_authorized_tasks_no_index_restriction() {
"expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(), "expiresAt": (OffsetDateTime::now_utc() + Duration::hours(1)).format(&Rfc3339).unwrap(),
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -475,7 +507,7 @@ async fn list_authorized_tasks_no_index_restriction() {
server.use_api_key(&key); server.use_api_key(&key);
let (response, code) = server.service.get("/tasks").await; let (response, code) = server.service.get("/tasks").await;
assert_eq!(code, 200); assert_eq!(200, code, "{:?}", &response);
let response = response["results"].as_array().unwrap(); let response = response["results"].as_array().unwrap();
// key should have access on `products` index. // key should have access on `products` index.
@ -498,7 +530,7 @@ async fn error_creating_index_without_action() {
"expiresAt": "2050-11-13T00:00:00Z" "expiresAt": "2050-11-13T00:00:00Z"
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -522,7 +554,7 @@ async fn error_creating_index_without_action() {
]); ]);
let (response, code) = index.add_documents(documents, None).await; 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 task_id = response["taskUid"].as_u64().unwrap();
let response = index.wait_task(task_id).await; let response = index.wait_task(task_id).await;
@ -533,7 +565,7 @@ async fn error_creating_index_without_action() {
let settings = json!({ "distinctAttribute": "test"}); let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await; 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 task_id = response["taskUid"].as_u64().unwrap();
let response = index.wait_task(task_id).await; let response = index.wait_task(task_id).await;
@ -543,7 +575,7 @@ async fn error_creating_index_without_action() {
// try to create a index via add specialized settings route // try to create a index via add specialized settings route
let (response, code) = index.update_distinct_attribute(json!("test")).await; 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 task_id = response["taskUid"].as_u64().unwrap();
let response = index.wait_task(task_id).await; let response = index.wait_task(task_id).await;
@ -565,7 +597,7 @@ async fn lazy_create_index() {
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
assert_eq!(code, 201); assert_eq!(201, code, "{:?}", &response);
assert!(response["key"].is_string()); assert!(response["key"].is_string());
// use created key. // use created key.
@ -582,13 +614,13 @@ async fn lazy_create_index() {
]); ]);
let (response, code) = index.add_documents(documents, None).await; 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 task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await; index.wait_task(task_id).await;
let (response, code) = index.get_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"); assert_eq!(response["status"], "succeeded");
// try to create a index via add settings route // try to create a index via add settings route
@ -596,24 +628,24 @@ async fn lazy_create_index() {
let settings = json!({ "distinctAttribute": "test"}); let settings = json!({ "distinctAttribute": "test"});
let (response, code) = index.update_settings(settings).await; 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 task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await; index.wait_task(task_id).await;
let (response, code) = index.get_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"); assert_eq!(response["status"], "succeeded");
// try to create a index via add specialized settings route // try to create a index via add specialized settings route
let index = server.index("test2"); let index = server.index("test2");
let (response, code) = index.update_distinct_attribute(json!("test")).await; 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 task_id = response["taskUid"].as_u64().unwrap();
index.wait_task(task_id).await; index.wait_task(task_id).await;
let (response, code) = index.get_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"); assert_eq!(response["status"], "succeeded");
} }

View File

@ -13,6 +13,15 @@ impl Server {
self.service.api_key = Some(api_key.as_ref().to_string()); 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<str>) {
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) { pub async fn add_api_key(&self, content: Value) -> (Value, StatusCode) {
let url = "/keys"; let url = "/keys";
self.service.post(url, content).await self.service.post(url, content).await

View File

@ -8,11 +8,15 @@ use time::{Duration, OffsetDateTime};
use super::authorization::{ALL_ACTIONS, AUTHORIZATIONS}; use super::authorization::{ALL_ACTIONS, AUTHORIZATIONS};
fn generate_tenant_token(parent_key: impl AsRef<str>, mut body: HashMap<&str, Value>) -> String { fn generate_tenant_token(
parent_uid: impl AsRef<str>,
parent_key: impl AsRef<str>,
mut body: HashMap<&str, Value>,
) -> String {
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
let key_id = &parent_key.as_ref()[..8]; let parent_uid = parent_uid.as_ref();
body.insert("apiKeyPrefix", json!(key_id)); body.insert("apiKeyUid", json!(parent_uid));
encode( encode(
&Header::default(), &Header::default(),
&body, &body,
@ -114,7 +118,7 @@ static REFUSED_KEYS: Lazy<Vec<Value>> = Lazy::new(|| {
macro_rules! compute_autorized_search { macro_rules! compute_autorized_search {
($tenant_tokens:expr, $filter:expr, $expected_count:expr) => { ($tenant_tokens:expr, $filter:expr, $expected_count:expr) => {
let mut server = Server::new_auth().await; 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 index = server.index("sales");
let documents = DOCUMENTS.clone(); let documents = DOCUMENTS.clone();
index.add_documents(documents, None).await; 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; let (response, code) = server.add_api_key(key_content.clone()).await;
assert_eq!(code, 201); assert_eq!(code, 201);
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
let uid = response["uid"].as_str().unwrap();
for tenant_token in $tenant_tokens.iter() { 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); server.use_api_key(&web_token);
let index = server.index("sales"); let index = server.index("sales");
index index
@ -160,7 +165,7 @@ macro_rules! compute_autorized_search {
macro_rules! compute_forbidden_search { macro_rules! compute_forbidden_search {
($tenant_tokens:expr, $parent_keys:expr) => { ($tenant_tokens:expr, $parent_keys:expr) => {
let mut server = Server::new_auth().await; 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 index = server.index("sales");
let documents = DOCUMENTS.clone(); let documents = DOCUMENTS.clone();
index.add_documents(documents, None).await; 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; let (response, code) = server.add_api_key(key_content.clone()).await;
assert_eq!(code, 201, "{:?}", response); assert_eq!(code, 201, "{:?}", response);
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
let uid = response["uid"].as_str().unwrap();
for tenant_token in $tenant_tokens.iter() { 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); server.use_api_key(&web_token);
let index = server.index("sales"); let index = server.index("sales");
index index
@ -461,12 +467,13 @@ async fn error_access_forbidden_routes() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
let uid = response["uid"].as_str().unwrap();
let tenant_token = hashmap! { let tenant_token = hashmap! {
"searchRules" => json!(["*"]), "searchRules" => json!(["*"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "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); server.use_api_key(&web_token);
for ((method, route), actions) in AUTHORIZATIONS.iter() { for ((method, route), actions) in AUTHORIZATIONS.iter() {
@ -496,12 +503,13 @@ async fn error_access_expired_parent_key() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
let uid = response["uid"].as_str().unwrap();
let tenant_token = hashmap! { let tenant_token = hashmap! {
"searchRules" => json!(["*"]), "searchRules" => json!(["*"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "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); server.use_api_key(&web_token);
// test search request while parent_key is not expired // test search request while parent_key is not expired
@ -538,12 +546,13 @@ async fn error_access_modified_token() {
assert!(response["key"].is_string()); assert!(response["key"].is_string());
let key = response["key"].as_str().unwrap(); let key = response["key"].as_str().unwrap();
let uid = response["uid"].as_str().unwrap();
let tenant_token = hashmap! { let tenant_token = hashmap! {
"searchRules" => json!(["products"]), "searchRules" => json!(["products"]),
"exp" => json!((OffsetDateTime::now_utc() + Duration::hours(1)).unix_timestamp()) "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); server.use_api_key(&web_token);
// test search request while web_token is valid // 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()) "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 = [ let altered_token = [
web_token.split('.').next().unwrap(), web_token.split('.').next().unwrap(),
alt.split('.').nth(1).unwrap(), alt.split('.').nth(1).unwrap(),

View File

@ -110,7 +110,7 @@ impl Index<'_> {
let url = format!("/tasks/{}", update_id); let url = format!("/tasks/{}", update_id);
for _ in 0..10 { for _ in 0..10 {
let (response, status_code) = self.service.get(&url).await; 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" { if response["status"] == "succeeded" || response["status"] == "failed" {
return response; return response;

View File

@ -52,7 +52,7 @@ tempfile = "3.3.0"
thiserror = "1.0.30" thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] } time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing", "macros"] }
tokio = { version = "1.17.0", features = ["full"] } 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" walkdir = "2.3.2"
whoami = { version = "1.2.1", optional = true } whoami = { version = "1.2.1", optional = true }

View File

@ -1,10 +1,12 @@
use std::fs::{self, create_dir_all, File}; use std::fs::{self, create_dir_all, File};
use std::io::Write; use std::io::{BufReader, Write};
use std::path::Path; use std::path::Path;
use fs_extra::dir::{self, CopyOptions}; use fs_extra::dir::{self, CopyOptions};
use log::info; use log::info;
use serde_json::{Deserializer, Map, Value};
use tempfile::tempdir; use tempfile::tempdir;
use uuid::Uuid;
use crate::dump::{compat, Metadata}; use crate::dump::{compat, Metadata};
use crate::options::IndexerOpts; use crate::options::IndexerOpts;
@ -24,14 +26,10 @@ pub fn load_dump(
let options = CopyOptions::default(); let options = CopyOptions::default();
// Indexes // Indexes
dir::copy(src.as_ref().join("indexes"), patched_dir.path(), &options)?; dir::copy(src.as_ref().join("indexes"), &patched_dir, &options)?;
// Index uuids // Index uuids
dir::copy( dir::copy(src.as_ref().join("index_uuids"), &patched_dir, &options)?;
src.as_ref().join("index_uuids"),
patched_dir.path(),
&options,
)?;
// Metadata // Metadata
fs::copy( fs::copy(
@ -43,13 +41,11 @@ pub fn load_dump(
patch_updates(&src, &patched_dir)?; patch_updates(&src, &patched_dir)?;
// Keys // Keys
if src.as_ref().join("keys").exists() { patch_keys(&src, &patched_dir)?;
fs::copy(src.as_ref().join("keys"), patched_dir.path().join("keys"))?;
}
super::v5::load_dump( super::v5::load_dump(
meta, meta,
patched_dir.path(), &patched_dir,
dst, dst,
index_db_size, index_db_size,
meta_env_size, meta_env_size,
@ -79,3 +75,29 @@ fn patch_updates(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result
Ok(()) Ok(())
} }
fn patch_keys(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> 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<String, Value> = 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(())
}