use crate::models::api_config::{ApiConfig, CacheConfig};
use actix_web::{web, HttpRequest};
use base64::{engine::general_purpose, Engine};
use hex;
use myc_core::{
domain::{
dtos::security_group::PermissionedRole,
entities::{KVArtifactRead, KVArtifactWrite},
},
use_cases::service::profile::{fetch_profile_from_email, ProfileResponse},
};
use myc_diesel::repositories::SqlAppModule;
use myc_http_tools::{responses::GatewayError, Email, Profile};
use myc_kv::repositories::KVAppModule;
use mycelium_base::entities::FetchResponseKind;
use openssl::sha::Sha256;
use shaku::HasComponent;
use uuid::Uuid;
#[tracing::instrument(
name = "recovery_profile_from_storage_engines",
fields(email = %email.redacted_email()),
skip(req, email)
)]
pub(crate) async fn recovery_profile_from_storage_engines(
req: HttpRequest,
email: Email,
tenant: Option<Uuid>,
roles: Option<Vec<PermissionedRole>>,
) -> Result<Profile, GatewayError> {
tracing::info!(stage = "identity.profile", "Resolving user profile");
let search_key =
hash_profile_request(email.to_owned(), tenant, roles.to_owned());
if let Some(profile) =
fetch_profile_from_cache(search_key.to_owned(), req.clone()).await
{
tracing::info!(
stage = "identity.profile",
outcome = "from_cache",
"Profile obtained from cache"
);
return Ok(profile);
}
let profile = fetch_profile_from_datastore(
req.clone(),
email.to_owned(),
tenant,
roles.to_owned(),
)
.await
.ok_or_else(|| {
GatewayError::Forbidden(
"User was authenticated but has not an account".to_string(),
)
})?;
cache_profile(search_key, profile.clone(), req.clone()).await;
tracing::info!(
stage = "identity.profile",
outcome = "resolved",
"Profile resolved via datastore"
);
tracing::trace!("Profile: {:?}", profile.profile_redacted());
Ok(profile)
}
#[tracing::instrument(name = "hash_profile_request", skip_all)]
fn hash_profile_request(
email: Email,
tenant: Option<Uuid>,
roles: Option<Vec<PermissionedRole>>,
) -> String {
let email = email.email();
let email_based_uuid = Uuid::new_v3(&Uuid::NAMESPACE_DNS, email.as_bytes());
let mut hasher = Sha256::new();
hasher.update(email.as_bytes());
let tenant = tenant.unwrap_or_else(|| email_based_uuid);
hasher.update(tenant.as_bytes());
let permissioned_roles = if let Some(roles) = roles {
roles
.iter()
.map(|role| {
format!(
"{r}:{p}",
r = role.name,
p = role.permission.clone().unwrap_or_default().to_i32()
)
})
.collect::<Vec<_>>()
.join("")
} else {
email_based_uuid.to_string()
};
hasher.update(permissioned_roles.as_bytes());
hex::encode(hasher.finish())
}
#[tracing::instrument(name = "fetch_profile_from_cache", skip_all)]
async fn fetch_profile_from_cache(
search_key: String,
req: HttpRequest,
) -> Option<Profile> {
tracing::trace!("Resolving profile from cache: {search_key}");
let app_module = match req.app_data::<web::Data<KVAppModule>>() {
Some(app_module) => app_module,
None => {
tracing::error!(
"Unable to extract profile fetching module from request"
);
return None;
}
};
let kv_artifact_read: &dyn KVArtifactRead = app_module.resolve_ref();
let profile_response =
match kv_artifact_read.get_encoded_artifact(search_key).await {
Err(err) => {
tracing::error!(
"Unexpected error on fetch profile from cache: {err}"
);
return None;
}
Ok(res) => res,
};
let profile_base64 = match profile_response {
FetchResponseKind::NotFound(_) => {
tracing::info!(
stage = "identity.profile.cache",
cache_hit = false,
"Profile cache: miss"
);
return None;
}
FetchResponseKind::Found(payload) => payload,
};
let profile_slice = match general_purpose::STANDARD.decode(profile_base64) {
Ok(res) => res,
Err(err) => {
tracing::warn!(
"Unexpected error on fetch profile from cache: {err}"
);
return None;
}
};
match serde_json::from_slice::<Profile>(&profile_slice) {
Ok(profile) => {
tracing::info!(
stage = "identity.profile.cache",
cache_hit = true,
"Profile cache: hit"
);
tracing::trace!("Cached profile: {:?}", profile.profile_redacted());
Some(profile)
}
Err(err) => {
tracing::warn!(
"Unexpected error on fetch profile from cache: {err}"
);
return None;
}
}
}
#[tracing::instrument(name = "fetch_profile_from_datastore", skip_all)]
async fn fetch_profile_from_datastore(
req: HttpRequest,
email: Email,
tenant: Option<Uuid>,
roles: Option<Vec<PermissionedRole>>,
) -> Option<Profile> {
tracing::trace!("Fetching profile from datastore");
let app_module = match req.app_data::<web::Data<SqlAppModule>>() {
Some(app_module) => app_module,
None => {
tracing::error!(
"Unable to extract profile fetching module from request"
);
return None;
}
};
match fetch_profile_from_email(
email.to_owned(),
None,
tenant,
roles,
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
)
.await
{
Ok(ProfileResponse::RegisteredUser(res)) => Some(res),
Ok(ProfileResponse::UnregisteredUser(_)) => None,
Err(err) => {
tracing::warn!(
"Unexpected error on fetch profile from email: {err}"
);
None
}
}
}
#[tracing::instrument(name = "cache_profile", skip_all)]
async fn cache_profile(search_key: String, profile: Profile, req: HttpRequest) {
tracing::trace!("Caching profile: {search_key}");
let app_module = match req.app_data::<web::Data<KVAppModule>>() {
Some(app_module) => app_module,
None => {
tracing::error!(
"Unable to extract profile caching module from request"
);
return;
}
};
let ttl = if let Some(api_config) = req.app_data::<web::Data<ApiConfig>>() {
let default_cache_config = CacheConfig::default();
let cache_config =
api_config.cache.as_ref().unwrap_or(&default_cache_config);
cache_config.profile_ttl.unwrap_or(60)
} else {
60
};
let kv_artifact_write: &dyn KVArtifactWrite = app_module.resolve_ref();
let serialized_profile = match serde_json::to_string(&profile) {
Ok(serialized_profile) => serialized_profile,
Err(err) => {
tracing::error!("Unexpected error on serialize profile: {err}");
return;
}
};
let encoded_profile =
general_purpose::STANDARD.encode(serialized_profile.as_bytes());
match kv_artifact_write
.set_encoded_artifact(search_key, encoded_profile, ttl)
.await
{
Ok(_) => (),
Err(err) => {
tracing::error!("Unexpected error on cache profile: {err}");
return;
}
}
}