use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::event::constants::partner_channel;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessagingIdentity {
User { partner_id: Uuid },
Guest { guest_id: Uuid },
}
impl MessagingIdentity {
pub fn channel(&self) -> String {
match self {
MessagingIdentity::User { partner_id } => partner_channel(*partner_id),
MessagingIdentity::Guest { guest_id } => {
crate::domain::event::constants::guest_channel(*guest_id)
}
}
}
pub fn partner_id(&self) -> Option<Uuid> {
match self {
MessagingIdentity::User { partner_id } => Some(*partner_id),
MessagingIdentity::Guest { .. } => None,
}
}
pub fn guest_id(&self) -> Option<Uuid> {
match self {
MessagingIdentity::Guest { guest_id } => Some(*guest_id),
MessagingIdentity::User { .. } => None,
}
}
}
#[async_trait]
pub trait ThreadAccessResolver: Send + Sync {
async fn can_read(
&self,
pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool;
async fn can_post(
&self,
pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool;
}
pub struct DenyHostDocs;
#[async_trait]
impl ThreadAccessResolver for DenyHostDocs {
async fn can_read(
&self,
_pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
is_own_partner_wall(identity, model, res_id)
}
async fn can_post(
&self,
_pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
is_own_partner_wall(identity, model, res_id)
}
}
fn is_own_partner_wall(identity: &MessagingIdentity, model: &str, res_id: Uuid) -> bool {
model == "res.partner" && identity.partner_id() == Some(res_id)
}
pub struct StaticThreadAccess {
pub open_models: Vec<String>,
}
#[async_trait]
impl ThreadAccessResolver for StaticThreadAccess {
async fn can_read(
&self,
_pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
is_own_partner_wall(identity, model, res_id)
|| (self.open_models.iter().any(|m| m == model) && identity.partner_id().is_some())
}
async fn can_post(
&self,
_pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
is_own_partner_wall(identity, model, res_id)
|| (self.open_models.iter().any(|m| m == model) && identity.partner_id().is_some())
}
}
#[derive(Clone)]
pub struct ThreadAclSlot {
inner: std::sync::Arc<std::sync::RwLock<Arc<dyn ThreadAccessResolver>>>,
}
impl ThreadAclSlot {
pub fn install(&self, resolver: Arc<dyn ThreadAccessResolver>) {
*self.inner.write().unwrap_or_else(|e| e.into_inner()) = resolver;
}
pub fn current(&self) -> Arc<dyn ThreadAccessResolver> {
self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
}
}
impl Default for ThreadAclSlot {
fn default() -> Self {
Self { inner: std::sync::Arc::new(std::sync::RwLock::new(Arc::new(DenyHostDocs))) }
}
}
#[async_trait::async_trait]
impl ThreadAccessResolver for ThreadAclSlot {
async fn can_read(
&self,
pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
self.current().can_read(pool, identity, model, res_id).await
}
async fn can_post(
&self,
pool: &sqlx::PgPool,
identity: &MessagingIdentity,
model: &str,
res_id: Uuid,
) -> bool {
self.current().can_post(pool, identity, model, res_id).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tid() -> Uuid {
Uuid::new_v4()
}
#[tokio::test]
async fn deny_host_docs_denies_document_chatter() {
let id = MessagingIdentity::User { partner_id: tid() };
let r = DenyHostDocs.can_read(&unreachable_pool(), &id, "crm.lead", tid()).await;
assert!(!r, "document chatter must be deny-by-default");
}
#[tokio::test]
async fn own_partner_wall_is_open() {
let p = tid();
let id = MessagingIdentity::User { partner_id: p };
assert!(DenyHostDocs.can_read(&unreachable_pool(), &id, "res.partner", p).await);
assert!(DenyHostDocs.can_post(&unreachable_pool(), &id, "res.partner", p).await);
assert!(!DenyHostDocs.can_read(&unreachable_pool(), &id, "res.partner", tid()).await);
}
#[tokio::test]
async fn guests_never_match_partner_wall() {
let id = MessagingIdentity::Guest { guest_id: tid() };
assert!(!DenyHostDocs.can_read(&unreachable_pool(), &id, "res.partner", tid()).await);
}
#[tokio::test]
async fn static_open_models_allows_authenticated_users_only() {
let s = StaticThreadAccess { open_models: vec!["project.project".into()] };
let u = MessagingIdentity::User { partner_id: tid() };
let g = MessagingIdentity::Guest { guest_id: tid() };
assert!(s.can_read(&unreachable_pool(), &u, "project.project", tid()).await);
assert!(!s.can_read(&unreachable_pool(), &g, "project.project", tid()).await);
assert!(!s.can_read(&unreachable_pool(), &u, "crm.lead", tid()).await);
}
fn unreachable_pool() -> sqlx::PgPool {
sqlx::PgPool::connect_lazy("postgres://nobody@127.0.0.1:1/none")
.unwrap_or_else(|_| panic!("lazy pool handle must always construct"))
}
}