Add uid and name fields in keys

This commit is contained in:
ManyTheFish 2022-05-25 10:32:47 +02:00
parent e2c204cf86
commit 96a5791e39
9 changed files with 205 additions and 177 deletions

1
Cargo.lock generated
View File

@ -1982,6 +1982,7 @@ dependencies = [
"sha2", "sha2",
"thiserror", "thiserror",
"time 0.3.9", "time 0.3.9",
"uuid",
] ]
[[package]] [[package]]

View File

@ -13,3 +13,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"] }

View File

@ -18,8 +18,16 @@ 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 uuidv4 string or ommited.")]
InvalidApiKeyUid(Value),
#[error("API key `{0}` not found.")] #[error("API key `{0}` not found.")]
ApiKeyNotFound(String), ApiKeyNotFound(String),
#[error("`uid` field value `{0}` already exists for an API key.")]
ApiKeyAlreadyExists(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 +47,10 @@ 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::Internal(_) => Code::Internal, Self::Internal(_) => Code::Internal,
} }
} }

View File

@ -1,18 +1,21 @@
use crate::action::Action; use crate::action::Action;
use crate::error::{AuthControllerError, Result}; use crate::error::{AuthControllerError, Result};
use crate::store::{KeyId, KEY_ID_LENGTH}; use crate::store::KeyId;
use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{from_value, Value}; use serde_json::{from_value, Value};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time}; use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Key { pub struct Key {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
pub id: KeyId, #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub uid: KeyId,
pub actions: Vec<Action>, pub actions: Vec<Action>,
pub indexes: Vec<String>, pub indexes: Vec<String>,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
@ -25,6 +28,15 @@ 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 name = match value.get("name") {
Some(Value::Null) => None,
Some(des) => Some(
from_value(des.clone())
.map_err(|_| AuthControllerError::InvalidApiKeyName(des.clone()))?,
),
None => None,
};
let description = match value.get("description") { let description = match value.get("description") {
Some(Value::Null) => None, Some(Value::Null) => None,
Some(des) => Some( Some(des) => Some(
@ -34,7 +46,13 @@ impl Key {
None => None, None => None,
}; };
let id = generate_id(); 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 +79,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,
@ -101,9 +120,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 {
name: Some("admin".to_string()),
description: Some("Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)".to_string()), description: Some("Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)".to_string()),
id: generate_id(), 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 +135,13 @@ 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 {
name: Some("search".to_string()),
description: Some( description: Some(
"Default Search API Key (Use it to search from the frontend)".to_string(), "Default Search API Key (Use it to search from the frontend)".to_string(),
), ),
id: generate_id(), uid,
actions: vec![Action::Search], actions: vec![Action::Search],
indexes: vec!["*".to_string()], indexes: vec!["*".to_string()],
expires_at: None, expires_at: None,
@ -128,19 +151,6 @@ impl Key {
} }
} }
/// Generate a printable key of 64 characters using thread_rng.
fn generate_id() -> [u8; KEY_ID_LENGTH] {
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut rng = rand::thread_rng();
let mut bytes = [0; KEY_ID_LENGTH];
for byte in bytes.iter_mut() {
*byte = CHARSET[rng.gen_range(0..CHARSET.len())];
}
bytes
}
fn parse_expiration_date(value: &Value) -> Result<Option<OffsetDateTime>> { fn parse_expiration_date(value: &Value) -> Result<Option<OffsetDateTime>> {
match value { match value {
Value::String(string) => OffsetDateTime::parse(string, &Rfc3339) Value::String(string) => OffsetDateTime::parse(string, &Rfc3339)

View File

@ -4,14 +4,15 @@ pub mod error;
mod key; mod key;
mod store; mod store;
use crate::store::generate_key;
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 uuid::Uuid;
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;
pub use action::{actions, Action}; pub use action::{actions, Action};
@ -42,62 +43,73 @@ 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_uid_from_sha(&self, key: &[u8]) -> Result<Option<Uuid>> {
match &self.master_key {
Some(master_key) => self.store.get_uid_from_sha(key, master_key.as_bytes()),
None => Ok(None),
}
}
pub fn try_get_uid_from_sha(&self, key: &str) -> Result<Uuid> {
self.get_uid_from_sha(key.as_bytes())?
.ok_or_else(|| AuthControllerError::ApiKeyNotFound(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 +117,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 +131,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(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 +166,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 +245,6 @@ pub struct IndexSearchRules {
pub filter: Option<serde_json::Value>, pub filter: Option<serde_json::Value>,
} }
fn generate_key(master_key: &[u8], keyid: &str) -> String {
let key = [keyid.as_bytes(), master_key].concat();
let sha = Sha256::digest(&key);
format!("{}{:x}", keyid, sha)
}
fn generate_default_keys(store: &HeedAuthStore) -> Result<()> { fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
store.put_api_key(Key::default_admin())?; store.put_api_key(Key::default_admin())?;
store.put_api_key(Key::default_search())?; store.put_api_key(Key::default_search())?;

View File

@ -1,4 +1,3 @@
use enum_iterator::IntoEnumIterator;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -8,20 +7,22 @@ use std::path::Path;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use enum_iterator::IntoEnumIterator;
use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson};
use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; use milli::heed::{Database, Env, EnvOpenOptions, RwTxn};
use sha2::{Digest, Sha256};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid;
use super::error::Result; use super::error::Result;
use super::{Action, Key}; use super::{Action, Key};
const AUTH_STORE_SIZE: usize = 1_073_741_824; //1GiB const AUTH_STORE_SIZE: usize = 1_073_741_824; //1GiB
pub const KEY_ID_LENGTH: usize = 8;
const AUTH_DB_PATH: &str = "auth"; const AUTH_DB_PATH: &str = "auth";
const KEY_DB_NAME: &str = "api-keys"; const KEY_DB_NAME: &str = "api-keys";
const KEY_ID_ACTION_INDEX_EXPIRATION_DB_NAME: &str = "keyid-action-index-expiration"; const KEY_ID_ACTION_INDEX_EXPIRATION_DB_NAME: &str = "keyid-action-index-expiration";
pub type KeyId = [u8; KEY_ID_LENGTH]; pub type KeyId = Uuid;
#[derive(Clone)] #[derive(Clone)]
pub struct HeedAuthStore { pub struct HeedAuthStore {
@ -73,12 +74,13 @@ impl HeedAuthStore {
} }
pub fn put_api_key(&self, key: Key) -> Result<Key> { pub fn put_api_key(&self, key: Key) -> Result<Key> {
let uid = key.uid;
let mut wtxn = self.env.write_txn()?; let mut wtxn = self.env.write_txn()?;
self.keys.put(&mut wtxn, &key.id, &key)?;
let id = key.id; self.keys.put(&mut wtxn, uid.as_bytes(), &key)?;
// delete key from inverted database before refilling it. // delete key from inverted database before refilling it.
self.delete_key_from_inverted_db(&mut wtxn, &id)?; self.delete_key_from_inverted_db(&mut wtxn, &uid)?;
// create inverted database. // create inverted database.
let db = self.action_keyid_index_expiration; let db = self.action_keyid_index_expiration;
@ -93,13 +95,13 @@ impl HeedAuthStore {
for action in actions { for action in actions {
if no_index_restriction { if no_index_restriction {
// If there is no index restriction we put None. // If there is no index restriction we put None.
db.put(&mut wtxn, &(&id, &action, None), &key.expires_at)?; db.put(&mut wtxn, &(&uid, &action, None), &key.expires_at)?;
} else { } else {
// else we create a key for each index. // else we create a key for each index.
for index in key.indexes.iter() { for index in key.indexes.iter() {
db.put( db.put(
&mut wtxn, &mut wtxn,
&(&id, &action, Some(index.as_bytes())), &(&uid, &action, Some(index.as_bytes())),
&key.expires_at, &key.expires_at,
)?; )?;
} }
@ -111,24 +113,33 @@ 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_sha(&self, key_sha: &[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(uid, master_key).as_bytes() == key_sha => {
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 +158,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 +206,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 +224,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 +235,12 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
} }
} }
pub fn generate_key(uid: &[u8], master_key: &[u8]) -> String {
let key = [uid, master_key].concat();
let sha = Sha256::digest(&key);
format!("{:x}", sha)
}
/// Divides one slice into two at an index, returns `None` if mid is out of bounds. /// Divides one slice into two at an index, returns `None` if mid is out of bounds.
pub fn try_split_at<T>(slice: &[T], mid: usize) -> Option<(&[T], &[T])> { pub fn try_split_at<T>(slice: &[T], mid: usize) -> Option<(&[T], &[T])> {
if mid <= slice.len() { if mid <= slice.len() {

View File

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

View File

@ -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,16 +147,16 @@ 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 from 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 { uid, .. } = token_data.claims;
Some(api_key_prefix) Some(uid)
} }
pub struct MasterPolicy; pub struct MasterPolicy;
@ -195,8 +196,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_uid_from_sha(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 +218,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 +245,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 +258,6 @@ pub mod policies {
struct Claims { struct Claims {
search_rules: SearchRules, search_rules: SearchRules,
exp: Option<i64>, exp: Option<i64>,
api_key_prefix: String, uid: Uuid,
} }
} }

View File

@ -1,4 +1,5 @@
use std::str; use std::str;
use uuid::Uuid;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
@ -20,7 +21,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.route(web::get().to(SeqHandler(list_api_keys))), .route(web::get().to(SeqHandler(list_api_keys))),
) )
.service( .service(
web::resource("/{api_key}") web::resource("/{key}")
.route(web::get().to(SeqHandler(get_api_key))) .route(web::get().to(SeqHandler(get_api_key)))
.route(web::patch().to(SeqHandler(patch_api_key))) .route(web::patch().to(SeqHandler(patch_api_key)))
.route(web::delete().to(SeqHandler(delete_api_key))), .route(web::delete().to(SeqHandler(delete_api_key))),
@ -65,9 +66,12 @@ pub async fn get_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<MasterPolicy, 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.try_get_uid_from_sha(&key))?;
let key = auth_controller.get_key(uid)?;
Ok(KeyView::from_key(key, &auth_controller)) Ok(KeyView::from_key(key, &auth_controller))
}) })
.await .await
@ -81,10 +85,12 @@ pub async fn patch_api_key(
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.try_get_uid_from_sha(&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
@ -97,24 +103,29 @@ pub async fn delete_api_key(
auth_controller: GuardedData<MasterPolicy, AuthController>, auth_controller: GuardedData<MasterPolicy, 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 = Uuid::parse_str(&key).or_else(|_| auth_controller.try_get_uid_from_sha(&key))?;
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??; 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 +138,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,