mirror of
https://github.com/meilisearch/meilisearch.git
synced 2024-11-23 02:27:40 +08:00
Merge #2438
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:
commit
08d72e32a4
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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]]
|
||||||
|
@ -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"] }
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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())?;
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -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;
|
||||||
|
@ -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 }
|
||||||
|
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user