use cratestack_core::{CoolContext, CoolError};
use crate::{FilterExpr, ModelDescriptor, ReadPolicy, SqlxRuntime, sqlx};
use super::filter::push_filter_query;
use super::policy::push_action_policy_query;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadPolicyKind {
List,
Detail,
}
pub(crate) fn push_scoped_conditions<'a, M, PK, Id>(
query: &mut sqlx::QueryBuilder<'a, sqlx::Postgres>,
descriptor: &ModelDescriptor<M, PK>,
filters: &[FilterExpr],
primary_key: Option<(&'static str, Id)>,
ctx: &CoolContext,
policy_kind: ReadPolicyKind,
) where
Id: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres> + 'a,
{
query.push(" WHERE ");
let mut wrote_clause = false;
if let Some(col) = descriptor.soft_delete_column {
query.push(col).push(" IS NULL");
wrote_clause = true;
}
if !filters.is_empty() {
if wrote_clause {
query.push(" AND ");
}
push_filter_query(query, filters);
wrote_clause = true;
}
if let Some((primary_key, id)) = primary_key {
if wrote_clause {
query.push(" AND ");
}
query.push(primary_key).push(" = ");
query.push_bind(id);
wrote_clause = true;
}
if wrote_clause {
query.push(" AND ");
}
let (allow, deny) = match policy_kind {
ReadPolicyKind::List => (
descriptor.read_allow_policies,
descriptor.read_deny_policies,
),
ReadPolicyKind::Detail => (
descriptor.detail_allow_policies,
descriptor.detail_deny_policies,
),
};
push_action_policy_query(query, allow, deny, ctx);
}
pub(crate) async fn authorize_record_action<M, PK>(
runtime: &SqlxRuntime,
descriptor: &'static ModelDescriptor<M, PK>,
id: PK,
allow_policies: &[ReadPolicy],
deny_policies: &[ReadPolicy],
ctx: &CoolContext,
action_name: &str,
) -> Result<(), CoolError>
where
PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
{
let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT 1 FROM ");
query
.push(descriptor.table_name)
.push(" WHERE ")
.push(descriptor.primary_key)
.push(" = ");
query.push_bind(id);
query.push(" AND ");
push_action_policy_query(&mut query, allow_policies, deny_policies, ctx);
query.push(" LIMIT 1");
let authorized = query
.build_query_scalar::<i32>()
.fetch_optional(runtime.pool())
.await
.map_err(|error| CoolError::Database(error.to_string()))?
.is_some();
if authorized {
Ok(())
} else {
Err(CoolError::Forbidden(format!(
"{action_name} policy denied this operation"
)))
}
}