use autumn_web::authorization::PolicyContext;
use autumn_web::search::IndexDefinition;
use crate::backend::{BoxFuture, SearchFilter};
use crate::error::{SearchError, SearchResult};
pub trait SearchVisibility: Send + Sync + 'static {
fn filter<'a>(
&'a self,
ctx: &'a PolicyContext,
index: &'a str,
) -> BoxFuture<'a, SearchResult<SearchFilter>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TenantVisibility;
impl SearchVisibility for TenantVisibility {
fn filter<'a>(
&'a self,
_ctx: &'a PolicyContext,
_index: &'a str,
) -> BoxFuture<'a, SearchResult<SearchFilter>> {
Box::pin(async move { Ok(current_tenant_filter()) })
}
}
pub fn ambient_tenant_filter(definition: &IndexDefinition) -> SearchResult<SearchFilter> {
let filter = current_tenant_filter();
if definition.tenant_scoped && filter.tenant_id.is_none() {
return Err(SearchError::TenantContextMissing {
index: definition.name.to_owned(),
});
}
Ok(filter)
}
#[must_use]
pub fn current_tenant_filter() -> SearchFilter {
autumn_web::tenancy::CURRENT_TENANT
.try_with(Clone::clone)
.ok()
.flatten()
.map_or_else(SearchFilter::default, |tenant| {
SearchFilter::default().tenant(tenant)
})
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DenyAll;
impl SearchVisibility for DenyAll {
fn filter<'a>(
&'a self,
_ctx: &'a PolicyContext,
_index: &'a str,
) -> BoxFuture<'a, SearchResult<SearchFilter>> {
Box::pin(async move { Ok(SearchFilter::default().allow_ids(Vec::<i64>::new())) })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn outside_a_tenant_scope_there_is_nothing_to_restrict() {
assert_eq!(current_tenant_filter(), SearchFilter::default());
}
#[tokio::test]
async fn inside_a_tenant_scope_the_filter_names_that_tenant() {
autumn_web::tenancy::CURRENT_TENANT
.scope(Some("acme".to_owned()), async {
assert_eq!(current_tenant_filter().tenant_id.as_deref(), Some("acme"));
})
.await;
}
#[tokio::test]
async fn a_tenant_scoped_index_refuses_an_unscoped_query() {
const FIELDS: &[autumn_web::search::SearchIndexField] =
&[autumn_web::search::SearchIndexField::new("title", 'A')];
let scoped = IndexDefinition::new("articles", "simple", FIELDS, None, true);
let unscoped = IndexDefinition::new("notes", "simple", FIELDS, None, false);
let error = ambient_tenant_filter(&scoped).expect_err("must refuse");
assert!(
matches!(error, SearchError::TenantContextMissing { ref index } if index == "articles"),
"{error:?}"
);
assert_eq!(
ambient_tenant_filter(&unscoped).expect("no tenant column"),
SearchFilter::default()
);
autumn_web::tenancy::CURRENT_TENANT
.scope(Some("acme".to_owned()), async {
let filter = ambient_tenant_filter(&scoped).expect("in scope");
assert_eq!(filter.tenant_id.as_deref(), Some("acme"));
})
.await;
}
#[tokio::test]
async fn deny_all_matches_nothing() {
let filter = SearchFilter::default().allow_ids(Vec::<i64>::new());
assert!(filter.matches_nothing());
}
}