Making it work with index uid patterns

This commit is contained in:
Kerollmops 2023-01-25 17:22:32 +01:00
parent ec7de4bae7
commit d563ed8a39
No known key found for this signature in database
GPG Key ID: 92ADA4E935E71FA4
6 changed files with 53 additions and 48 deletions

1
Cargo.lock generated
View File

@ -2535,6 +2535,7 @@ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"enum-iterator", "enum-iterator",
"hmac", "hmac",
"maplit",
"meilisearch-types", "meilisearch-types",
"rand", "rand",
"roaring", "roaring",

View File

@ -7,6 +7,7 @@ edition = "2021"
base64 = "0.13.1" base64 = "0.13.1"
enum-iterator = "1.1.3" enum-iterator = "1.1.3"
hmac = "0.12.1" hmac = "0.12.1"
maplit = "1.0.2"
meilisearch-types = { path = "../meilisearch-types" } meilisearch-types = { path = "../meilisearch-types" }
rand = "0.8.5" rand = "0.8.5"
roaring = { version = "0.10.0", features = ["serde"] } roaring = { version = "0.10.0", features = ["serde"] }

View File

@ -8,6 +8,7 @@ use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use error::{AuthControllerError, Result}; use error::{AuthControllerError, Result};
use maplit::hashset;
use meilisearch_types::index_uid_pattern::IndexUidPattern; use meilisearch_types::index_uid_pattern::IndexUidPattern;
use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey};
use meilisearch_types::star_or::StarOr; use meilisearch_types::star_or::StarOr;
@ -75,31 +76,12 @@ impl AuthController {
search_rules: Option<SearchRules>, search_rules: Option<SearchRules>,
) -> Result<AuthFilter> { ) -> Result<AuthFilter> {
let mut filters = AuthFilter::default(); let mut filters = AuthFilter::default();
let key = self let key = self.get_key(uid)?;
.store
.get_api_key(uid)?
.ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?;
if !key.indexes.iter().any(|i| i == &StarOr::Star) { filters.search_rules = match search_rules {
filters.search_rules = match search_rules { Some(search_rules) => search_rules,
// Intersect search_rules with parent key authorized indexes. None => SearchRules::Set(key.indexes.into_iter().collect()),
Some(search_rules) => SearchRules::Map( };
key.indexes
.into_iter()
.filter_map(|index| {
search_rules.get_index_search_rules(index.deref()).map(
|index_search_rules| {
(String::from(index), Some(index_search_rules))
},
)
})
.collect(),
),
None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()),
};
} else if let Some(search_rules) = search_rules {
filters.search_rules = search_rules;
}
filters.allow_index_creation = self.is_key_authorized(uid, Action::IndexesAdd, None)?; filters.allow_index_creation = self.is_key_authorized(uid, Action::IndexesAdd, None)?;
@ -182,13 +164,13 @@ impl Default for AuthFilter {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)] #[serde(untagged)]
pub enum SearchRules { pub enum SearchRules {
Set(HashSet<String>), Set(HashSet<IndexUidPattern>),
Map(HashMap<String, Option<IndexSearchRules>>), Map(HashMap<IndexUidPattern, Option<IndexSearchRules>>),
} }
impl Default for SearchRules { impl Default for SearchRules {
fn default() -> Self { fn default() -> Self {
Self::Set(Some("*".to_string()).into_iter().collect()) Self::Set(hashset! { IndexUidPattern::all() })
} }
} }
@ -198,16 +180,12 @@ impl SearchRules {
Self::Set(set) => { Self::Set(set) => {
set.contains("*") set.contains("*")
|| set.contains(index) || set.contains(index)
|| set || set.iter().any(|pattern| pattern.matches_str(index))
.iter() // We must store the IndexUidPattern in the Set
.any(|pattern| IndexUidPattern::new_unchecked(pattern).matches_str(index))
} }
Self::Map(map) => { Self::Map(map) => {
map.contains_key("*") map.contains_key("*")
|| map.contains_key(index) || map.contains_key(index)
|| map || map.keys().any(|pattern| pattern.matches_str(index))
.keys() // We must store the IndexUidPattern in the Map
.any(|pattern| IndexUidPattern::new_unchecked(pattern).matches_str(index))
} }
} }
} }
@ -215,21 +193,26 @@ impl SearchRules {
pub fn get_index_search_rules(&self, index: &str) -> Option<IndexSearchRules> { pub fn get_index_search_rules(&self, index: &str) -> Option<IndexSearchRules> {
match self { match self {
Self::Set(set) => { Self::Set(set) => {
if set.contains("*") || set.contains(index) { if self.is_index_authorized(index) {
Some(IndexSearchRules::default()) Some(IndexSearchRules::default())
} else { } else {
None None
} }
} }
Self::Map(map) => { Self::Map(map) => {
map.get(index).or_else(|| map.get("*")).map(|isr| isr.clone().unwrap_or_default()) // We must take the most retrictive rule of this index uid patterns set of rules.
map.iter()
.filter(|(pattern, _)| pattern.matches_str(index))
.max_by_key(|(pattern, _)| (pattern.is_exact(), pattern.len()))
.map(|(_, rule)| rule.clone())
.flatten()
} }
} }
} }
/// Return the list of indexes such that `self.is_index_authorized(index) == true`, /// Return the list of indexes such that `self.is_index_authorized(index) == true`,
/// or `None` if all indexes satisfy this condition. /// or `None` if all indexes satisfy this condition.
pub fn authorized_indexes(&self) -> Option<Vec<String>> { pub fn authorized_indexes(&self) -> Option<Vec<IndexUidPattern>> {
match self { match self {
SearchRules::Set(set) => { SearchRules::Set(set) => {
if set.contains("*") { if set.contains("*") {
@ -250,7 +233,7 @@ impl SearchRules {
} }
impl IntoIterator for SearchRules { impl IntoIterator for SearchRules {
type Item = (String, IndexSearchRules); type Item = (IndexUidPattern, IndexSearchRules);
type IntoIter = Box<dyn Iterator<Item = Self::Item>>; type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {

View File

@ -1,7 +1,10 @@
use std::borrow::Borrow;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use deserr::DeserializeFromValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::error::{Code, ErrorCode}; use crate::error::{Code, ErrorCode};
@ -9,17 +12,25 @@ use crate::index_uid::{IndexUid, IndexUidFormatError};
/// An index uid pattern is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// An index uid pattern is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long and optionally ending with a *. /// bytes long and optionally ending with a *.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, DeserializeFromValue, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] #[deserr(from(&String) = FromStr::from_str -> IndexUidPatternFormatError)]
pub struct IndexUidPattern( pub struct IndexUidPattern(String);
#[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}\\*?")))] String,
);
impl IndexUidPattern { impl IndexUidPattern {
pub fn new_unchecked(s: impl AsRef<str>) -> Self { pub fn new_unchecked(s: impl AsRef<str>) -> Self {
Self(s.as_ref().to_string()) Self(s.as_ref().to_string())
} }
/// Matches any index name.
pub fn all() -> Self {
IndexUidPattern::from_str("*").unwrap()
}
/// Returns `true` if the pattern matches a specific index name.
pub fn is_exact(&self) -> bool {
!self.0.ends_with('*')
}
/// Returns wether this index uid matches this index uid pattern. /// Returns wether this index uid matches this index uid pattern.
pub fn matches(&self, uid: &IndexUid) -> bool { pub fn matches(&self, uid: &IndexUid) -> bool {
self.matches_str(uid.as_str()) self.matches_str(uid.as_str())
@ -34,7 +45,7 @@ impl IndexUidPattern {
} }
} }
impl std::ops::Deref for IndexUidPattern { impl Deref for IndexUidPattern {
type Target = str; type Target = str;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -42,6 +53,12 @@ impl std::ops::Deref for IndexUidPattern {
} }
} }
impl Borrow<str> for IndexUidPattern {
fn borrow(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for IndexUidPattern { impl TryFrom<String> for IndexUidPattern {
type Error = IndexUidPatternFormatError; type Error = IndexUidPatternFormatError;

View File

@ -47,7 +47,7 @@ pub struct CreateApiKey {
#[deserr(error = DeserrError<InvalidApiKeyActions>)] #[deserr(error = DeserrError<InvalidApiKeyActions>)]
pub actions: Vec<Action>, pub actions: Vec<Action>,
#[deserr(error = DeserrError<InvalidApiKeyIndexes>)] #[deserr(error = DeserrError<InvalidApiKeyIndexes>)]
pub indexes: Vec<StarOr<IndexUidPattern>>, pub indexes: Vec<IndexUidPattern>,
#[deserr(error = DeserrError<InvalidApiKeyExpiresAt>, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage<ParseOffsetDateTimeError>)] #[deserr(error = DeserrError<InvalidApiKeyExpiresAt>, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage<ParseOffsetDateTimeError>)]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
} }
@ -109,7 +109,7 @@ pub struct Key {
pub name: Option<String>, pub name: Option<String>,
pub uid: KeyId, pub uid: KeyId,
pub actions: Vec<Action>, pub actions: Vec<Action>,
pub indexes: Vec<StarOr<IndexUidPattern>>, pub indexes: Vec<IndexUidPattern>,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
@ -127,7 +127,7 @@ impl Key {
description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()), description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()),
uid, uid,
actions: vec![Action::All], actions: vec![Action::All],
indexes: vec![StarOr::Star], indexes: vec![IndexUidPattern::all()],
expires_at: None, expires_at: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
@ -142,7 +142,7 @@ impl Key {
description: Some("Use it to search from the frontend".to_string()), description: Some("Use it to search from the frontend".to_string()),
uid, uid,
actions: vec![Action::Search], actions: vec![Action::Search],
indexes: vec![StarOr::Star], indexes: vec![IndexUidPattern::all()],
expires_at: None, expires_at: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,

View File

@ -230,7 +230,10 @@ pub mod policies {
} }
} }
return auth.get_key_filters(uid, Some(data.claims.search_rules)).ok(); match auth.get_key_filters(uid, Some(data.claims.search_rules)) {
Ok(auth) if auth.search_rules.is_index_authorized() => Some(auth),
_ => None,
}
} }
None None