use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use async_trait::async_trait;
use cloudproof_findex::{
IndexedValue, Keyword, Location,
implementations::redis::{FindexRedis, FindexRedisError, RemovedLocationsFinder},
parameters::MASTER_KEY_LENGTH,
};
use cosmian_kmip::kmip_2_1::KmipOperation;
use cosmian_kms_crypto::reexport::cosmian_crypto_core::{FixedSizeCBytes, SymmetricKey};
use crate::{DbError, error::DbResult};
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Triple {
obj_uid: String,
user_id: String,
permission: KmipOperation,
}
impl Triple {
pub(crate) fn new(obj_uid: &str, user_id: &str, permission: KmipOperation) -> Self {
Self {
obj_uid: obj_uid.to_owned(),
user_id: user_id.to_owned(),
permission,
}
}
pub(crate) fn key(&self) -> String {
Self::build_key(&self.obj_uid, &self.user_id)
}
pub(crate) fn build_key(obj_uid: &str, user_id: &str) -> String {
format!("{obj_uid}::{user_id}")
}
pub(crate) fn permissions_per_user(
list: HashSet<Self>,
) -> HashMap<String, HashSet<KmipOperation>> {
let mut map = HashMap::new();
for triple in list {
let entry = map.entry(triple.user_id).or_insert_with(HashSet::new);
entry.insert(triple.permission);
}
map
}
pub(crate) fn permissions_per_object(
list: HashSet<Self>,
) -> HashMap<String, HashSet<KmipOperation>> {
let mut map = HashMap::new();
for triple in list {
let entry = map.entry(triple.obj_uid).or_insert_with(HashSet::new);
entry.insert(triple.permission);
}
map
}
}
impl TryFrom<&Location> for Triple {
type Error = DbError;
fn try_from(value: &Location) -> Result<Self, Self::Error> {
let value = String::from_utf8((value).to_vec())?;
let mut parts = value.split("::");
let uid = parts.next().ok_or_else(|| {
DbError::ConversionError(format!("invalid permissions triple: {parts:?}"))
})?;
let user_id = parts.next().ok_or_else(|| {
DbError::ConversionError(format!("invalid permissions triple: {parts:?}"))
})?;
let permission = parts.next().ok_or_else(|| {
DbError::ConversionError(format!("invalid permissions triple: {parts:?}"))
})?;
Ok(Self {
obj_uid: uid.to_owned(),
user_id: user_id.to_owned(),
permission: serde_json::from_str(permission)?,
})
}
}
impl TryFrom<&Triple> for Location {
type Error = DbError;
fn try_from(value: &Triple) -> Result<Self, Self::Error> {
Ok(Self::from(
format!(
"{}::{}::{}",
value.obj_uid,
value.user_id,
serde_json::to_string(&value.permission)?
)
.into_bytes(),
))
}
}
#[derive(Clone)]
pub(crate) struct PermissionsDB {
findex: Arc<FindexRedis>,
label: Vec<u8>,
}
impl PermissionsDB {
pub(crate) fn new(findex: Arc<FindexRedis>, label: &[u8]) -> Self {
Self {
findex,
label: label.to_vec(),
}
}
async fn search_one_keyword(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
keyword: &str,
) -> DbResult<HashSet<Triple>> {
let keyword = Keyword::from(format!("p::{keyword}").as_bytes());
self.findex
.search(
&findex_key.to_bytes(),
&self.label,
HashSet::from([keyword.clone()]),
)
.await?
.into_iter()
.next()
.unwrap_or_else(|| (keyword, HashSet::new()))
.1
.iter()
.map(Triple::try_from)
.collect::<DbResult<HashSet<Triple>>>()
}
pub(crate) async fn list_user_permissions(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
user_id: &str,
) -> DbResult<HashMap<String, HashSet<KmipOperation>>> {
Ok(Triple::permissions_per_object(
self.search_one_keyword(findex_key, user_id).await?,
))
}
pub(crate) async fn list_object_permissions(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
obj_uid: &str,
) -> DbResult<HashMap<String, HashSet<KmipOperation>>> {
Ok(Triple::permissions_per_user(
self.search_one_keyword(findex_key, obj_uid).await?,
))
}
pub(crate) async fn get(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
obj_uid: &str,
user_id: &str,
no_inherited_access: bool,
) -> DbResult<HashSet<KmipOperation>> {
let mut user_perms = self
.search_one_keyword(findex_key, &Triple::build_key(obj_uid, user_id))
.await?
.into_iter()
.map(|triple| triple.permission)
.collect::<HashSet<KmipOperation>>();
if no_inherited_access {
return Ok(user_perms);
}
let wildcard_user_perms = self
.search_one_keyword(findex_key, &Triple::build_key(obj_uid, "*"))
.await?
.into_iter()
.map(|triple| triple.permission)
.collect::<HashSet<KmipOperation>>();
user_perms.extend(wildcard_user_perms);
Ok(user_perms)
}
pub(crate) async fn add(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
obj_uid: &str,
user_id: &str,
permission: KmipOperation,
) -> DbResult<()> {
let triple = Triple::new(obj_uid, user_id, permission);
let indexed_value = IndexedValue::from(Location::try_from(&triple)?);
let keyword = Keyword::from(format!("p::{}", triple.key()).as_bytes());
let mut additions = HashMap::new();
additions.insert(indexed_value, HashSet::from([keyword.clone()]));
let new_keywords = self
.findex
.upsert(
&findex_key.to_bytes(),
&self.label,
additions,
HashMap::new(),
)
.await?;
let is_already_present = !new_keywords.contains(&keyword);
if is_already_present {
return Ok(());
}
let mut additions = HashMap::new();
additions.insert(
IndexedValue::from(keyword),
HashSet::from([
Keyword::from(format!("p::{obj_uid}").as_bytes()),
Keyword::from(format!("p::{user_id}").as_bytes()),
]),
);
self.findex
.upsert(
&findex_key.to_bytes(),
&self.label,
additions,
HashMap::new(),
)
.await?;
Ok(())
}
pub(crate) async fn remove(
&self,
findex_key: &SymmetricKey<MASTER_KEY_LENGTH>,
obj_uid: &str,
user_id: &str,
permission: KmipOperation,
) -> DbResult<()> {
let triple = Triple::new(obj_uid, user_id, permission);
let indexed_value = IndexedValue::from(Location::try_from(&triple)?);
let keyword = Keyword::from(format!("p::{}", triple.key()).as_bytes());
let mut deletions = HashMap::new();
deletions.insert(indexed_value, HashSet::from([keyword.clone()]));
let new_keywords = self
.findex
.upsert(
&findex_key.to_bytes(),
&self.label,
HashMap::new(),
deletions,
)
.await?;
let is_new = new_keywords.contains(&keyword);
if is_new {
let mut additions = HashMap::new();
additions.insert(
IndexedValue::from(keyword),
HashSet::from([
Keyword::from(format!("p::{obj_uid}").as_bytes()),
Keyword::from(format!("p::{user_id}").as_bytes()),
]),
);
self.findex
.upsert(
&findex_key.to_bytes(),
&self.label,
additions,
HashMap::new(),
)
.await?;
}
Ok(())
}
}
#[async_trait]
impl RemovedLocationsFinder for PermissionsDB {
async fn find_removed_locations(
&self,
_locations: HashSet<Location>,
) -> Result<HashSet<Location>, FindexRedisError> {
Ok(HashSet::new())
}
}