use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{
db::WhereClause,
error::{FraiseQLError, Result},
security::SecurityContext,
utils::clock::{Clock, SystemClock},
};
#[derive(Debug, Clone, PartialEq)]
pub struct RlsWhereClause {
inner: WhereClause,
}
impl RlsWhereClause {
pub(crate) const fn new(inner: WhereClause) -> Self {
Self { inner }
}
#[must_use]
pub const fn as_where_clause(&self) -> &WhereClause {
&self.inner
}
#[must_use]
pub fn into_where_clause(self) -> WhereClause {
self.inner
}
}
#[derive(Debug, Clone)]
pub(crate) struct CacheEntry {
pub(crate) result: Option<WhereClause>,
pub(crate) expires_at: u64,
}
pub trait RLSPolicy: Send + Sync {
fn evaluate(
&self,
context: &SecurityContext,
type_name: &str,
) -> Result<Option<RlsWhereClause>>;
fn cache_result(&self, _cache_key: &str, _result: &Option<WhereClause>) {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultRLSPolicy {
pub enable_tenant_isolation: bool,
pub tenant_field: String,
pub owner_field: String,
}
impl DefaultRLSPolicy {
#[must_use]
pub fn new() -> Self {
Self {
enable_tenant_isolation: true,
tenant_field: "tenant_id".to_string(),
owner_field: "author_id".to_string(),
}
}
#[must_use]
pub const fn with_single_tenant(mut self) -> Self {
self.enable_tenant_isolation = false;
self
}
#[must_use]
pub fn with_tenant_field(mut self, field: String) -> Self {
self.tenant_field = field;
self
}
#[must_use]
pub fn with_owner_field(mut self, field: String) -> Self {
self.owner_field = field;
self
}
}
impl Default for DefaultRLSPolicy {
fn default() -> Self {
Self::new()
}
}
impl RLSPolicy for DefaultRLSPolicy {
fn evaluate(
&self,
context: &SecurityContext,
_type_name: &str,
) -> Result<Option<RlsWhereClause>> {
if context.is_admin() {
return Ok(None);
}
let mut filters = vec![];
if self.enable_tenant_isolation {
if let Some(ref tenant_id) = context.tenant_id {
filters.push(WhereClause::Field {
path: vec![self.tenant_field.clone()],
operator: crate::db::WhereOperator::Eq,
value: serde_json::json!(tenant_id.clone()),
});
}
}
filters.push(WhereClause::Field {
path: vec![self.owner_field.clone()],
operator: crate::db::WhereOperator::Eq,
value: serde_json::json!(context.user_id.clone()),
});
let clause = match filters.len() {
0 => return Ok(None),
1 => filters.into_iter().next().expect("len checked == 1"),
_ => WhereClause::And(filters),
};
Ok(Some(RlsWhereClause::new(clause)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoRLSPolicy;
impl RLSPolicy for NoRLSPolicy {
fn evaluate(
&self,
_context: &SecurityContext,
_type_name: &str,
) -> Result<Option<RlsWhereClause>> {
Ok(None)
}
}
fn default_system_clock() -> Arc<dyn Clock> {
Arc::new(SystemClock)
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CompiledRLSPolicy {
pub rules_by_type: std::collections::HashMap<String, Vec<RLSRule>>,
pub default_rule: Option<RLSRule>,
#[serde(skip)]
pub(crate) cache: Arc<parking_lot::RwLock<std::collections::HashMap<String, CacheEntry>>>,
#[serde(skip, default = "default_system_clock")]
clock: Arc<dyn Clock>,
}
impl std::fmt::Debug for CompiledRLSPolicy {
#[cfg_attr(test, mutants::skip)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompiledRLSPolicy")
.field("rules_by_type", &self.rules_by_type)
.field("default_rule", &self.default_rule)
.field("cache", &"<cached>")
.field("clock", &"<clock>")
.finish()
}
}
impl CompiledRLSPolicy {
#[must_use]
pub fn new(
rules_by_type: std::collections::HashMap<String, Vec<RLSRule>>,
default_rule: Option<RLSRule>,
) -> Self {
Self::new_with_clock(rules_by_type, default_rule, Arc::new(SystemClock))
}
pub fn new_with_clock(
rules_by_type: std::collections::HashMap<String, Vec<RLSRule>>,
default_rule: Option<RLSRule>,
clock: Arc<dyn Clock>,
) -> Self {
Self {
rules_by_type,
default_rule,
cache: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
clock,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RLSRule {
pub name: String,
pub expression: String,
pub cacheable: bool,
pub cache_ttl_seconds: Option<u64>,
}
impl RLSPolicy for CompiledRLSPolicy {
fn evaluate(
&self,
context: &SecurityContext,
type_name: &str,
) -> Result<Option<RlsWhereClause>> {
if context.is_admin() {
return Ok(None);
}
let rule = self
.rules_by_type
.get(type_name)
.and_then(|rules| rules.first())
.or(self.default_rule.as_ref());
if let Some(rule) = rule {
let cache_key = if rule.cacheable {
Some(format!("{}:{}", context.user_id, type_name))
} else {
None
};
if let Some(ref key) = cache_key {
let cache = self.cache.read();
if let Some(entry) = cache.get(key) {
if self.clock.now_secs() < entry.expires_at {
return Ok(entry.result.clone().map(RlsWhereClause::new));
}
}
drop(cache);
}
let result: Option<WhereClause> = evaluate_rls_expression(&rule.expression, context)?;
if let Some(key) = cache_key {
if let Some(ttl_secs) = rule.cache_ttl_seconds {
let expires_at = self.clock.now_secs() + ttl_secs;
let entry = CacheEntry {
result: result.clone(),
expires_at,
};
let mut cache = self.cache.write();
cache.insert(key, entry);
}
}
Ok(result.map(RlsWhereClause::new))
} else {
Ok(None)
}
}
fn cache_result(&self, cache_key: &str, result: &Option<WhereClause>) {
let expires_at = self.clock.now_secs() + 300;
let entry = CacheEntry {
result: result.clone(),
expires_at,
};
let mut cache = self.cache.write();
cache.insert(cache_key.to_string(), entry);
}
}
fn evaluate_rls_expression(
expression: &str,
context: &SecurityContext,
) -> Result<Option<WhereClause>> {
let expr = expression.trim();
if let Some(eq_parts) = expr.split_once("==") {
let left = eq_parts.0.trim();
let right = eq_parts.1.trim();
if let Some(user_field) = left.strip_prefix("user.") {
let user_value = extract_user_value(user_field, context);
if let Some(object_field) = right.strip_prefix("object.") {
return Ok(Some(WhereClause::Field {
path: vec![object_field.to_string()],
operator: crate::db::WhereOperator::Eq,
value: user_value.unwrap_or(serde_json::Value::Null),
}));
} else if serde_json::from_str::<serde_json::Value>(right).is_ok() {
return Ok(Some(WhereClause::Field {
path: vec!["_literal_".to_string()],
operator: crate::db::WhereOperator::Eq,
value: serde_json::json!(user_value),
}));
}
}
}
if expr.contains("includes") {
if let Some(includes_parts) = expr.split_once("includes") {
let left = includes_parts.0.trim();
let right = includes_parts.1.trim().trim_matches(|c| c == '\'' || c == '"');
if left == "user.roles" && context.has_role(right) {
return Ok(None);
}
}
}
if expr.contains("tenant_id") && expr.contains("==") {
if let Some(tenant_id) = &context.tenant_id {
return Ok(Some(WhereClause::Field {
path: vec!["tenant_id".to_string()],
operator: crate::db::WhereOperator::Eq,
value: serde_json::json!(tenant_id),
}));
}
}
Err(FraiseQLError::Validation {
message: format!("Unrecognised RLS expression: '{expr}'"),
path: None,
})
}
pub(crate) fn extract_user_value(
field: &str,
context: &SecurityContext,
) -> Option<serde_json::Value> {
match field {
"id" | "user_id" => Some(serde_json::json!(context.user_id)),
"tenant_id" => context.tenant_id.as_ref().map(|t| serde_json::json!(t)),
"roles" => Some(serde_json::json!(context.roles)),
custom => context.get_attribute(custom).cloned(),
}
}