use std::collections::{HashMap, HashSet};
use super::{super::context::ExecutorContext, query::QueryRunner};
use crate::{
backend::{
COMPOSED_DOCUMENT_KEY, COMPOSED_EMBEDS_KEY, ComposedEmbed, ComposedLevel, EmbedShape,
EmbedSource, LevelKeys, WhereClause,
},
error::{FraiseQLError, Result},
graphql::FieldSelection,
runtime::{field_filter::FieldAccessResult, projection::effective_selections},
schema::{
CompiledSchema, FieldDefinition, FieldType, QueryDefinition, Relationship, TypeDefinition,
},
security::{ConstrainedPaths, RLSPolicy, SecurityContext, rls_policy::RlsTarget},
};
#[derive(Clone, Copy)]
pub(in super::super) struct LevelAuthz<'a> {
authorizer: &'a dyn crate::security::Authorizer,
input: Option<&'a serde_json::Value>,
}
impl<'a> LevelAuthz<'a> {
pub(in super::super) fn from_config(
config: &'a crate::runtime::RuntimeConfig,
input: Option<&'a serde_json::Value>,
) -> Option<Self> {
config.authorizer.as_deref().map(|authorizer| Self { authorizer, input })
}
}
pub(in super::super) struct SelectionAccess {
pub(super) root: FieldAccessResult,
masked: HashSet<(String, String)>,
}
impl SelectionAccess {
pub(in super::super) fn classify(
schema: &CompiledSchema,
root_type: &str,
root_fields: &[FieldSelection],
projection_keys: Vec<String>,
security_context: Option<&SecurityContext>,
authz: Option<LevelAuthz<'_>>,
) -> Result<Self> {
let mut masked = HashSet::new();
let mut level = Level {
schema,
security_context,
authz,
asked: HashSet::new(),
};
classify_level(&mut level, root_type, root_fields, "", &mut masked)?;
let masked_keys = effective_selections(root_fields, root_type, schema)
.into_iter()
.filter(|sel| masked.contains(&(root_type.to_string(), sel.name.clone())))
.map(|sel| sel.response_key().to_string())
.collect();
Ok(Self {
root: FieldAccessResult {
projected: projection_keys,
masked: masked_keys,
},
masked,
})
}
pub(in super::super) fn classify_roots(
schema: &CompiledSchema,
roots: &[(&str, &[FieldSelection])],
security_context: Option<&SecurityContext>,
authz: Option<LevelAuthz<'_>>,
) -> Result<Self> {
let mut masked = HashSet::new();
let mut level = Level {
schema,
security_context,
authz,
asked: HashSet::new(),
};
for (root_type, root_fields) in roots {
classify_level(&mut level, root_type, root_fields, "", &mut masked)?;
}
Ok(Self {
root: FieldAccessResult {
projected: Vec::new(),
masked: Vec::new(),
},
masked,
})
}
pub(in super::super) fn masked_fields(&self, type_name: &str) -> Vec<String> {
self.masked
.iter()
.filter(|(t, _)| t == type_name)
.map(|(_, field)| field.clone())
.collect()
}
pub(in super::super) fn null_masked(
&self,
value: &mut serde_json::Value,
root_type: &str,
root_fields: &[FieldSelection],
schema: &CompiledSchema,
) {
if !self.masked.is_empty() {
null_masked_at(value, root_type, root_fields, &self.masked, schema);
}
}
}
pub(super) fn object_type_of(field_type: &FieldType) -> Option<&str> {
match field_type.inner_type().unwrap_or(field_type) {
FieldType::Object(name) | FieldType::Interface(name) | FieldType::Union(name) => Some(name),
_ => None,
}
}
struct Level<'a> {
schema: &'a CompiledSchema,
security_context: Option<&'a SecurityContext>,
authz: Option<LevelAuthz<'a>>,
asked: HashSet<(String, String)>,
}
fn classify_level(
level_ctx: &mut Level<'_>,
type_name: &str,
selections: &[FieldSelection],
path: &str,
masked: &mut HashSet<(String, String)>,
) -> Result<()> {
let schema = level_ctx.schema;
let security_context = level_ctx.security_context;
let level: Vec<&FieldSelection> = effective_selections(selections, type_name, schema)
.into_iter()
.filter(|sel| sel.name != "__typename")
.collect();
let names = level.iter().map(|sel| sel.name.clone()).collect();
let access = super::super::support::security::classify_fields_for_read(
schema,
type_name,
names,
security_context,
)?;
masked.extend(access.masked.into_iter().map(|field| (type_name.to_string(), field)));
let Some(type_def) = schema.find_type(type_name) else {
return Ok(());
};
for sel in level {
let child = type_def
.fields
.iter()
.find(|f| f.name == sel.name)
.and_then(|f| object_type_of(&f.field_type));
if let Some(child) = child {
enforce_level_read_gates(schema, type_name, &sel.name, child, security_context)?;
let child_path = if path.is_empty() {
sel.name.clone()
} else {
format!("{path}.{}", sel.name)
};
if let Some(authz) = level_ctx.authz {
if level_ctx.asked.insert((type_name.to_string(), child_path.clone())) {
ask_level_authorizer(
authz,
schema,
type_name,
child,
&child_path,
security_context,
)?;
}
}
classify_level(level_ctx, child, &sel.nested_fields, &child_path, masked)?;
}
}
Ok(())
}
fn ask_level_authorizer(
authz: LevelAuthz<'_>,
schema: &CompiledSchema,
parent_type: &str,
target: &str,
path: &str,
security_context: Option<&SecurityContext>,
) -> Result<()> {
let name = super::query_composed::list_query_for_type(schema, target)
.map_or(target, |q| q.name.as_str());
let op = crate::security::AuthzOperation::nested(
name,
target,
crate::security::AuthzNesting::new(parent_type, path),
);
crate::security::authorizer::enforce_authz(
authz.authorizer,
security_context,
&[op],
authz.input,
)
}
fn enforce_level_read_gates(
schema: &CompiledSchema,
parent_type: &str,
field: &str,
target: &str,
security_context: Option<&SecurityContext>,
) -> Result<()> {
match type_read_refusal(schema, target, security_context) {
None => Ok(()),
Some(why) => Err(FraiseQLError::Authorization {
message: format!("'{parent_type}.{field}' reads '{target}', {why}"),
action: Some("read".to_string()),
resource: Some(target.to_string()),
}),
}
}
pub(in super::super) fn type_read_refusal(
schema: &CompiledSchema,
type_name: &str,
security_context: Option<&SecurityContext>,
) -> Option<&'static str> {
let read = own_read(schema, type_name);
let role = read
.and_then(|q| q.requires_role.as_deref())
.or_else(|| schema.find_type(type_name).and_then(|t| t.requires_role.as_deref()));
if let Some(role) = role {
if !security_context.is_some_and(|ctx| ctx.roles.iter().any(|r| r == role)) {
return Some("whose read requires a role the request does not hold");
}
}
let actor_refused = read.is_some_and(|read| {
crate::security::actor_type::enforce_requires_actor(
"Query",
&read.name,
&read.requires_actor,
security_context,
)
.is_err()
});
actor_refused.then_some("whose read is restricted to actor types the request is not")
}
fn null_masked_at(
value: &mut serde_json::Value,
type_name: &str,
selections: &[FieldSelection],
masked: &HashSet<(String, String)>,
schema: &CompiledSchema,
) {
match value {
serde_json::Value::Array(items) => {
for item in items {
null_masked_at(item, type_name, selections, masked, schema);
}
},
serde_json::Value::Object(object) => {
let type_def = schema.find_type(type_name);
for sel in effective_selections(selections, type_name, schema) {
let key = sel.response_key();
if masked.contains(&(type_name.to_string(), sel.name.clone())) {
if let Some(slot) = object.get_mut(key) {
*slot = serde_json::Value::Null;
}
continue;
}
let child = type_def
.and_then(|t| t.fields.iter().find(|f| f.name == sel.name))
.and_then(|f| object_type_of(&f.field_type));
if let (Some(child), Some(nested)) = (child, object.get_mut(key)) {
null_masked_at(nested, child, &sel.nested_fields, masked, schema);
}
}
},
_ => {},
}
}
#[derive(Debug, Clone)]
pub(in super::super) enum NestedRowGate {
Project {
query: String,
paths: Vec<String>,
},
Join {
query: String,
view: String,
relationship: Relationship,
},
Refuse {
query: String,
},
}
impl NestedRowGate {
fn query(&self) -> &str {
match self {
Self::Project { query, .. } | Self::Join { query, .. } | Self::Refuse { query } => {
query
},
}
}
}
#[derive(Debug, Default)]
pub(in super::super) struct NestedRowGates {
by_field: HashMap<(String, String), NestedRowGate>,
}
impl NestedRowGates {
pub(in super::super) fn build(schema: &CompiledSchema, policy: Option<&dyn RLSPolicy>) -> Self {
let mut by_field = HashMap::new();
for parent in &schema.types {
for field in &parent.fields {
let Some(target) = object_type_of(&field.field_type) else {
continue;
};
if let Some(gate) = row_gate(schema, policy, parent, field, target) {
by_field.insert((parent.name.to_string(), field.name.to_string()), gate);
}
}
}
Self { by_field }
}
fn get(&self, parent: &str, field: &str) -> Option<&NestedRowGate> {
self.by_field.get(&(parent.to_string(), field.to_string()))
}
}
pub(in super::super) fn own_read<'a>(
schema: &'a CompiledSchema,
type_name: &str,
) -> Option<&'a QueryDefinition> {
let reads = |q: &&QueryDefinition| {
q.return_type == type_name
&& q.function.is_none()
&& q.sql_source.is_some()
&& !q.returns_count
};
schema
.queries
.iter()
.filter(reads)
.find(|q| q.returns_list)
.or_else(|| schema.queries.iter().find(reads))
}
fn row_gate(
schema: &CompiledSchema,
policy: Option<&dyn RLSPolicy>,
parent: &TypeDefinition,
field: &FieldDefinition,
target: &str,
) -> Option<NestedRowGate> {
let read = own_read(schema, target)?;
if policy.is_none() && read.inject_params.is_empty() {
return None;
}
let mut paths = match policy.map(|p| p.constrained_paths(&RlsTarget::query(&read.name, target)))
{
None => Some(Vec::new()),
Some(ConstrainedPaths::Declared(paths)) => Some(paths),
Some(ConstrainedPaths::Opaque) => None,
};
for column in read.inject_params.keys() {
if read.native_columns.contains_key(column) {
paths = None;
} else if let Some(paths) = paths.as_mut() {
paths.push(crate::utils::to_snake_case(column));
}
}
let declared = schema.find_type(target);
let carried = paths.filter(|paths| {
paths.iter().all(|path| {
declared.is_some_and(|t| {
t.fields.iter().any(|f| crate::utils::to_snake_case(f.name.as_str()) == *path)
})
})
});
if let Some(paths) = carried {
return Some(NestedRowGate::Project {
query: read.name.clone(),
paths,
});
}
let is_list = field.field_type.is_list();
let relationship = parent.relationships.iter().find(|r| {
r.name == field.name && r.target_type == target && r.cardinality.is_to_one() != is_list
});
let list = schema.queries.iter().find(|q| {
q.return_type == target && q.returns_list && q.function.is_none() && !q.returns_count
});
Some(match (relationship, list.and_then(|q| q.sql_source.as_ref().map(|v| (q, v)))) {
(Some(relationship), Some((list, view))) => NestedRowGate::Join {
query: list.name.clone(),
view: view.clone(),
relationship: relationship.clone(),
},
_ => NestedRowGate::Refuse {
query: read.name.clone(),
},
})
}
impl SelectionAccess {
fn masks(&self, type_name: &str, field: &str) -> bool {
self.masked.contains(&(type_name.to_string(), field.to_string()))
}
}
impl QueryRunner {
pub(super) fn plan_nested_reads(
&self,
root_type: &str,
root_fields: &[FieldSelection],
security_context: Option<&SecurityContext>,
access: &SelectionAccess,
) -> Result<Vec<ComposedEmbed>> {
self.plan_nested_level(root_type, root_fields, security_context, access)
}
fn plan_nested_level(
&self,
parent_type: &str,
selections: &[FieldSelection],
security_context: Option<&SecurityContext>,
access: &SelectionAccess,
) -> Result<Vec<ComposedEmbed>> {
let schema = &self.ctx.schema;
let Some(parent_def) = schema.find_type(parent_type) else {
return Ok(Vec::new());
};
let mut fields: Vec<(&FieldDefinition, Vec<FieldSelection>)> = Vec::new();
for sel in effective_selections(selections, parent_type, schema) {
let Some(field) = parent_def.fields.iter().find(|f| f.name == sel.name) else {
continue;
};
if object_type_of(&field.field_type).is_none() {
continue;
}
match fields.iter_mut().find(|(f, _)| f.name == field.name) {
Some((_, subs)) => subs.extend(sel.nested_fields.iter().cloned()),
None => fields.push((field, sel.nested_fields.clone())),
}
}
let mut embeds = Vec::new();
for (field, subs) in fields {
let Some(target) = object_type_of(&field.field_type) else {
continue;
};
let children = self.plan_nested_level(target, &subs, security_context, access)?;
let gate = self.ctx.nested_row_gates.get(parent_type, field.name.as_str());
let (predicate, source, view) = match gate {
None => (None, None, None),
Some(NestedRowGate::Project { query, paths }) => {
let predicate = self.nested_predicate(query, target, security_context)?;
if let Some(clause) = predicate.as_ref() {
if !reads_only(clause, paths) {
return Err(FraiseQLError::Authorization {
message: format!(
"the row-security predicate for '{target}' reads a key its \
policy does not declare, so it cannot be evaluated over the \
'{target}' documents '{parent_type}.{}' embeds; refusing \
rather than serving them unfiltered",
field.name
),
action: Some("read".to_string()),
resource: Some(target.to_string()),
});
}
}
(predicate, None, None)
},
Some(NestedRowGate::Join {
query,
view,
relationship,
}) => {
let predicate = self.nested_predicate(query, target, security_context)?;
let source =
super::query_composed::correlated_source(schema, relationship, parent_type);
(predicate, Some(source), Some(view.clone()))
},
Some(NestedRowGate::Refuse { query }) => {
let predicate = self.nested_predicate(query, target, security_context)?;
if predicate.is_some() {
return Err(FraiseQLError::Authorization {
message: format!(
"'{parent_type}.{}' embeds '{target}', whose row-security policy \
does not declare the keys it reads, and '{parent_type}' declares \
no relationship '{}' to read '{target}' through; refusing rather \
than serving '{target}' rows unfiltered (declare the \
relationship, or implement `RLSPolicy::constrained_paths`)",
field.name, field.name
),
action: Some("read".to_string()),
resource: Some(target.to_string()),
});
}
(None, None, None)
},
};
if source.is_none() && predicate.is_none() && children.is_empty() {
continue;
}
let (stored, fallback) = crate::runtime::stored_key_candidates(field.name.as_str());
let source = source.unwrap_or_else(|| EmbedSource::Materialised {
keys: std::iter::once(stored.clone()).chain(fallback).collect(),
});
let view = view.unwrap_or_else(|| {
schema.find_type(target).map_or_else(String::new, |t| t.sql_source.to_string())
});
let keys = level_keys(schema, target, &subs, &children, access);
embeds.push(ComposedEmbed {
output_key: stored,
shape: if field.field_type.is_list() {
EmbedShape::Many
} else {
EmbedShape::One
},
source,
level: ComposedLevel {
view,
projection: None,
where_clause: predicate,
order_by: None,
limit: None,
offset: None,
keyset: None,
keys,
embeds: children,
},
});
}
Ok(embeds)
}
#[cfg(feature = "federation")]
pub(in super::super) fn refuse_row_gated_levels(
&self,
parent_type: &str,
selections: &[FieldSelection],
security_context: Option<&SecurityContext>,
surface: &str,
) -> Result<()> {
let schema = &self.ctx.schema;
let Some(parent_def) = schema.find_type(parent_type) else {
return Ok(());
};
for sel in effective_selections(selections, parent_type, schema) {
let Some(field) = parent_def.fields.iter().find(|f| f.name == sel.name) else {
continue;
};
let Some(target) = object_type_of(&field.field_type) else {
continue;
};
if let Some(gate) = self.ctx.nested_row_gates.get(parent_type, field.name.as_str()) {
if self.nested_predicate(gate.query(), target, security_context)?.is_some() {
return Err(FraiseQLError::Authorization {
message: format!(
"{surface} cannot apply the row security of '{target}' to the \
'{target}' rows '{parent_type}.{}' embeds; refusing rather than \
serving them unfiltered",
field.name
),
action: Some("read".to_string()),
resource: Some(target.to_string()),
});
}
}
self.refuse_row_gated_levels(target, &sel.nested_fields, security_context, surface)?;
}
Ok(())
}
fn nested_predicate(
&self,
query: &str,
target: &str,
security_context: Option<&SecurityContext>,
) -> Result<Option<WhereClause>> {
level_predicate(&self.ctx, query, target, security_context)
}
}
fn level_predicate(
ctx: &ExecutorContext,
query: &str,
target: &str,
security_context: Option<&SecurityContext>,
) -> Result<Option<WhereClause>> {
row_predicate(&ctx.schema, ctx.config.rls_policy.as_deref(), query, target, security_context)
}
fn row_predicate(
schema: &CompiledSchema,
policy: Option<&dyn RLSPolicy>,
query: &str,
target: &str,
security_context: Option<&SecurityContext>,
) -> Result<Option<WhereClause>> {
let Some(read) = schema.queries.iter().find(|q| q.name == query) else {
return Ok(None);
};
let mut conditions = Vec::new();
match (policy, security_context) {
(Some(policy), Some(principal)) => {
if let Some(clause) =
policy.evaluate(principal, &RlsTarget::query(&read.name, target))?
{
conditions.push(clause.into_where_clause());
}
},
(Some(_), None) => {
return Err(FraiseQLError::Validation {
message: format!("Query '{}' not found in schema", read.name),
path: None,
});
},
(None, _) => {},
}
if !read.inject_params.is_empty() {
let Some(principal) = security_context else {
return Err(FraiseQLError::Validation {
message: format!(
"Type '{target}' is scoped by the inject params of query '{}', which a \
request without a security context cannot resolve",
read.name
),
path: None,
});
};
for (column, source) in &read.inject_params {
let value = super::super::resolve_inject_value(column, source, principal)?;
conditions.push(super::query_params::inject_param_where_clause(
column,
value,
&read.native_columns,
));
}
}
Ok(match conditions.len() {
0 => None,
1 => conditions.pop(),
_ => Some(WhereClause::And(conditions)),
})
}
pub(in super::super) fn guard_relation_filters(
schema: &CompiledSchema,
policy: Option<&dyn RLSPolicy>,
root_type: &str,
clause: &WhereClause,
security_context: Option<&SecurityContext>,
) -> Result<WhereClause> {
let guard =
|c: &WhereClause| guard_relation_filters(schema, policy, root_type, c, security_context);
Ok(match clause {
WhereClause::And(all) => WhereClause::And(all.iter().map(guard).collect::<Result<_>>()?),
WhereClause::Or(all) => WhereClause::Or(all.iter().map(guard).collect::<Result<_>>()?),
WhereClause::Not(inner) => WhereClause::Not(Box::new(guard(inner)?)),
WhereClause::Typed { types, inner } => WhereClause::Typed {
types: types.clone(),
inner: Box::new(guard(inner)?),
},
WhereClause::Field { path, .. } => {
relation_guards(schema, policy, root_type, path, security_context)?
.into_iter()
.rev()
.fold(clause.clone(), |inner, (under, guard)| WhereClause::Guarded {
under,
guard: Box::new(guard),
inner: Box::new(inner),
})
},
_ => clause.clone(),
})
}
fn relation_guards(
schema: &CompiledSchema,
policy: Option<&dyn RLSPolicy>,
root_type: &str,
path: &[String],
security_context: Option<&SecurityContext>,
) -> Result<Vec<(Vec<String>, WhereClause)>> {
let mut guards = Vec::new();
let mut current = root_type.to_string();
for depth in 0..path.len().saturating_sub(1) {
let Some(parent) = schema.find_type(¤t) else {
break;
};
let Some(field) = parent
.fields
.iter()
.find(|f| crate::utils::to_snake_case(f.name.as_str()) == path[depth])
else {
break;
};
let Some(target) = object_type_of(&field.field_type) else {
break;
};
if field.field_type.is_list() {
break;
}
if let Some(gate) = row_gate(schema, policy, parent, field, target) {
if let Some(predicate) =
row_predicate(schema, policy, gate.query(), target, security_context)?
{
let under = path[..=depth].to_vec();
let guard = match &gate {
NestedRowGate::Project { paths, .. } if reads_only(&predicate, paths) => {
under_path(&predicate, &under)
},
NestedRowGate::Join {
view, relationship, ..
} => {
let (target_key, parent_key, key_type) =
super::query_composed::correlation_keys(
schema,
relationship,
parent.name.as_str(),
);
WhereClause::KeyIn {
path: path[..depth].iter().chain(&parent_key).cloned().collect(),
key_type,
view: view.clone(),
target_key,
predicate: Box::new(predicate),
}
},
_ => {
return Err(FraiseQLError::Authorization {
message: format!(
"Access denied: '{}.{}' cannot be used to filter by: which \
'{target}' rows the request may read cannot be decided over the \
document the view embeds (declare the policy's keys with \
`RLSPolicy::constrained_paths`, or declare '{}' as a \
relationship of '{}')",
parent.name, field.name, field.name, parent.name
),
action: Some("read".to_string()),
resource: Some(target.to_string()),
});
},
};
guards.push((under, guard));
}
}
current = target.to_string();
}
Ok(guards)
}
fn under_path(clause: &WhereClause, under: &[String]) -> WhereClause {
match clause {
WhereClause::Field {
path,
operator,
value,
} => WhereClause::Field {
path: under.iter().chain(path).cloned().collect(),
operator: operator.clone(),
value: value.clone(),
},
WhereClause::And(all) => {
WhereClause::And(all.iter().map(|c| under_path(c, under)).collect())
},
WhereClause::Or(all) => WhereClause::Or(all.iter().map(|c| under_path(c, under)).collect()),
WhereClause::Not(inner) => WhereClause::Not(Box::new(under_path(inner, under))),
WhereClause::Typed { types, inner } => WhereClause::Typed {
types: types.clone(),
inner: Box::new(under_path(inner, under)),
},
other => other.clone(),
}
}
fn reads_only(clause: &WhereClause, paths: &[String]) -> bool {
match clause {
WhereClause::Field { path, .. } => path.first().is_some_and(|key| paths.contains(key)),
WhereClause::And(all) | WhereClause::Or(all) => all.iter().all(|c| reads_only(c, paths)),
WhereClause::Not(inner) | WhereClause::Typed { inner, .. } => reads_only(inner, paths),
_ => false,
}
}
fn level_keys(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
embedded: &[ComposedEmbed],
access: &SelectionAccess,
) -> LevelKeys {
let mut kept = Vec::new();
let mut masked = Vec::new();
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
continue;
}
let (stored, fallback) = crate::runtime::stored_key_candidates(&sel.name);
if embedded.iter().any(|e| e.output_key == stored) {
continue;
}
let into = if access.masks(type_name, &sel.name) {
&mut masked
} else {
&mut kept
};
for key in std::iter::once(stored).chain(fallback) {
if !into.contains(&key) {
into.push(key);
}
}
}
LevelKeys::Only { kept, masked }
}
pub(super) fn root_keys(embedded: &[ComposedEmbed]) -> LevelKeys {
LevelKeys::Without(
embedded
.iter()
.flat_map(|e| match &e.source {
EmbedSource::Materialised { keys } => keys.clone(),
EmbedSource::Correlated { .. } => {
let (stored, fallback) = crate::runtime::stored_key_candidates(&e.output_key);
std::iter::once(stored).chain(fallback).collect()
},
})
.collect(),
)
}
pub(super) fn merge_composed_row(
row: &serde_json::Value,
embeds: &[ComposedEmbed],
) -> serde_json::Value {
let mut document = row.get(COMPOSED_DOCUMENT_KEY).cloned().unwrap_or(serde_json::Value::Null);
let embedded = row.get(COMPOSED_EMBEDS_KEY);
if let serde_json::Value::Object(object) = &mut document {
for embed in embeds {
let raw = embedded.and_then(|e| e.get(&embed.output_key));
let value = match (embed.shape, raw) {
(EmbedShape::Many, Some(serde_json::Value::Array(rows))) => {
serde_json::Value::Array(
rows.iter().map(|r| merge_composed_row(r, &embed.level.embeds)).collect(),
)
},
(EmbedShape::Many, _) => serde_json::Value::Array(Vec::new()),
(EmbedShape::One, Some(row @ serde_json::Value::Object(_))) => {
merge_composed_row(row, &embed.level.embeds)
},
_ => serde_json::Value::Null,
};
object.insert(embed.output_key.clone(), value);
}
}
document
}
impl QueryRunner {
pub(super) async fn execute_composed_document_read(
&self,
root: ComposedLevel,
session_pairs: &[(&str, &str)],
routing: crate::backend::types::ReadRouting,
) -> Result<std::sync::Arc<Vec<crate::backend::JsonbValue>>> {
if !self.ctx.adapter.supports_composed_reads() {
return Err(crate::backend::composed_read_unsupported(&root.view));
}
let rows = self
.ctx
.adapter
.execute_composed_with_session(&root, session_pairs, routing)
.await?;
Ok(std::sync::Arc::new(
rows.iter()
.map(|row| {
crate::backend::JsonbValue::new(merge_composed_row(&row.data, &root.embeds))
})
.collect(),
))
}
}
pub(super) fn project_documents(
documents: &[crate::backend::JsonbValue],
root_type: &str,
root_fields: &[FieldSelection],
schema: &CompiledSchema,
returns_list: bool,
) -> serde_json::Value {
let project = |document: &crate::backend::JsonbValue| {
crate::runtime::project_entity(&document.data, root_type, root_fields, schema)
};
if returns_list {
serde_json::Value::Array(documents.iter().map(project).collect())
} else {
documents.first().map_or(serde_json::Value::Null, project)
}
}
const MAX_LEAF_OBJECT_DEPTH: usize = 4;
pub(super) fn expand_leaf_objects<'a>(
schema: &CompiledSchema,
query_match: &'a crate::runtime::QueryMatch,
) -> Result<std::borrow::Cow<'a, crate::runtime::QueryMatch>> {
let root_type = &query_match.query_def.return_type;
let Some(type_def) = schema.find_type(root_type) else {
return Ok(std::borrow::Cow::Borrowed(query_match));
};
let root_fields =
query_match.selections.first().map_or(&[][..], |r| r.nested_fields.as_slice());
let is_leaf_object = |sel: &FieldSelection| {
sel.nested_fields.is_empty()
&& type_def
.fields
.iter()
.find(|f| f.name == sel.name)
.is_some_and(|f| object_type_of(&f.field_type).is_some())
};
if !root_fields.iter().any(is_leaf_object) {
return Ok(std::borrow::Cow::Borrowed(query_match));
}
let mut expanded = query_match.clone();
if let Some(root) = expanded.selections.first_mut() {
for sel in &mut root.nested_fields {
if is_leaf_object(sel) {
let child = type_def
.fields
.iter()
.find(|f| f.name == sel.name)
.and_then(|f| object_type_of(&f.field_type));
if let Some(child) = child {
sel.nested_fields = whole_object(schema, child, 1)?;
}
}
}
}
Ok(std::borrow::Cow::Owned(expanded))
}
pub(in super::super) fn whole_object(
schema: &CompiledSchema,
type_name: &str,
depth: usize,
) -> Result<Vec<FieldSelection>> {
let Some(type_def) = schema.find_type(type_name) else {
return Ok(Vec::new());
};
type_def
.fields
.iter()
.map(|field| {
let nested_fields = match object_type_of(&field.field_type) {
None => Vec::new(),
Some(child) if depth >= MAX_LEAF_OBJECT_DEPTH => {
return Err(FraiseQLError::Validation {
message: format!(
"The whole object selected reaches '{type_name}.{}', a '{child}' \
{depth} levels down; a leaf object selection expands at most \
{MAX_LEAF_OBJECT_DEPTH} levels",
field.name
),
path: None,
});
},
Some(child) => whole_object(schema, child, depth + 1)?,
};
Ok(FieldSelection {
name: field.name.to_string(),
alias: None,
arguments: Vec::new(),
nested_fields,
directives: Vec::new(),
})
})
.collect()
}
pub(super) fn project_object_fields(
projected: &mut serde_json::Value,
rows: &[crate::backend::JsonbValue],
root_type: &str,
root_fields: &[FieldSelection],
schema: &CompiledSchema,
) {
let Some(type_def) = schema.find_type(root_type) else {
return;
};
let objects: Vec<(&FieldSelection, &str)> =
effective_selections(root_fields, root_type, schema)
.into_iter()
.filter(|sel| !sel.nested_fields.is_empty())
.filter_map(|sel| {
type_def
.fields
.iter()
.find(|f| f.name == sel.name)
.and_then(|f| object_type_of(&f.field_type))
.map(|child| (sel, child))
})
.collect();
if objects.is_empty() {
return;
}
let reproject = |item: &mut serde_json::Value, row: &crate::backend::JsonbValue| {
let Some(object) = item.as_object_mut() else {
return;
};
for (sel, child) in &objects {
let (stored, fallback) = crate::runtime::stored_key_candidates(&sel.name);
let raw = row.data.get(&stored).or_else(|| fallback.and_then(|k| row.data.get(&k)));
let value = match raw {
Some(serde_json::Value::Array(elements)) => serde_json::Value::Array(
elements
.iter()
.map(|el| {
crate::runtime::project_entity(el, child, &sel.nested_fields, schema)
})
.collect(),
),
Some(value) => {
crate::runtime::project_entity(value, child, &sel.nested_fields, schema)
},
None => continue,
};
object.insert(sel.response_key().to_string(), value);
}
};
match projected {
serde_json::Value::Array(items) => {
for (item, row) in items.iter_mut().zip(rows) {
reproject(item, row);
}
},
item @ serde_json::Value::Object(_) => {
if let Some(row) = rows.first() {
reproject(item, row);
}
},
_ => {},
}
}
type DocumentCondition = (Vec<String>, serde_json::Value);
#[derive(Debug, Default)]
pub(in super::super) struct DocumentRowFilter {
by_field: HashMap<(String, String), Vec<DocumentCondition>>,
}
impl DocumentRowFilter {
pub(in super::super) fn plan(
&mut self,
ctx: &ExecutorContext,
root_type: &str,
selections: &[FieldSelection],
security_context: Option<&SecurityContext>,
) -> Result<()> {
let schema = &ctx.schema;
let Some(parent_def) = schema.find_type(root_type) else {
return Ok(());
};
for sel in effective_selections(selections, root_type, schema) {
let Some(field) = parent_def.fields.iter().find(|f| f.name == sel.name) else {
continue;
};
let Some(target) = object_type_of(&field.field_type) else {
continue;
};
let key = (root_type.to_string(), field.name.to_string());
if let std::collections::hash_map::Entry::Vacant(slot) = self.by_field.entry(key) {
if let Some(gate) = ctx.nested_row_gates.get(root_type, field.name.as_str()) {
let predicate = level_predicate(ctx, gate.query(), target, security_context)?;
if let Some(clause) = predicate {
let refuse = |why: &str| FraiseQLError::Authorization {
message: format!(
"'{root_type}.{}' embeds '{target}' in the document the write \
returned, and {why}; refusing before the write rather than \
serving '{target}' rows unfiltered",
field.name
),
action: Some("read".to_string()),
resource: Some(target.to_string()),
};
let NestedRowGate::Project { paths, .. } = gate else {
return Err(refuse(
"the row-security policy of that type does not declare the \
keys it reads, so it cannot be evaluated over that document",
));
};
if !reads_only(&clause, paths) {
return Err(refuse(
"its row-security predicate reads a key its policy does not \
declare",
));
}
let mut conditions = Vec::new();
if !equalities(&clause, &mut conditions) {
return Err(refuse(
"its row-security predicate is not a conjunction of \
equalities, the only shape evaluated over a returned document",
));
}
slot.insert(conditions);
}
}
}
self.plan(ctx, target, &sel.nested_fields, security_context)?;
}
Ok(())
}
pub(in super::super) fn is_empty(&self) -> bool {
self.by_field.is_empty()
}
pub(in super::super) fn apply(
&self,
document: &mut serde_json::Value,
root_type: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) {
if self.by_field.is_empty() {
return;
}
let Some(parent_def) = schema.find_type(root_type) else {
return;
};
let serde_json::Value::Object(object) = document else {
return;
};
for sel in effective_selections(selections, root_type, schema) {
let Some(field) = parent_def.fields.iter().find(|f| f.name == sel.name) else {
continue;
};
let Some(target) = object_type_of(&field.field_type) else {
continue;
};
let (stored, fallback) = crate::runtime::stored_key_candidates(field.name.as_str());
let key = if object.contains_key(&stored) {
stored
} else if let Some(fallback) = fallback.filter(|k| object.contains_key(k)) {
fallback
} else {
continue;
};
let conditions = self.by_field.get(&(root_type.to_string(), field.name.to_string()));
let Some(value) = object.get_mut(&key) else {
continue;
};
match value {
serde_json::Value::Array(elements) => {
if let Some(conditions) = conditions {
elements.retain(|el| meets(el, conditions));
}
for element in elements {
self.apply(element, target, &sel.nested_fields, schema);
}
},
serde_json::Value::Null => {},
embedded => {
if conditions.is_some_and(|c| !meets(embedded, c)) {
*embedded = serde_json::Value::Null;
} else {
self.apply(embedded, target, &sel.nested_fields, schema);
}
},
}
}
}
}
#[derive(Debug)]
pub(in super::super) struct RootRowFilter {
conditions: Vec<DocumentCondition>,
}
impl RootRowFilter {
pub(in super::super) fn plan(
ctx: &ExecutorContext,
subscription: &str,
type_name: &str,
principal: Option<&SecurityContext>,
) -> Result<Option<Self>> {
let schema = &ctx.schema;
let policy = ctx.config.rls_policy.as_deref();
let read = own_read(schema, type_name);
let target = read.map_or_else(
|| RlsTarget::query(subscription, type_name),
|read| RlsTarget::query(&read.name, type_name),
);
let clause = match read {
Some(read) => level_predicate(ctx, &read.name, type_name, principal)?,
None => match (policy, principal) {
(Some(policy), Some(principal)) => policy
.evaluate(principal, &target)?
.map(crate::security::RlsWhereClause::into_where_clause),
(Some(_), None) => {
return Err(FraiseQLError::Validation {
message: format!("Subscription '{subscription}' not found in schema"),
path: None,
});
},
(None, _) => None,
},
};
let Some(clause) = clause else {
return Ok(None);
};
let refuse = |why: &str| FraiseQLError::Authorization {
message: format!(
"Subscription '{subscription}' delivers '{type_name}', whose row-security \
predicate {why}; refusing the subscription rather than delivering rows \
unfiltered"
),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
};
let mut paths = match policy.map(|p| p.constrained_paths(&target)) {
None => Vec::new(),
Some(ConstrainedPaths::Declared(paths)) => paths,
Some(ConstrainedPaths::Opaque) => {
return Err(refuse("does not declare the keys it reads"));
},
};
for column in read.map(|r| r.inject_params.keys()).into_iter().flatten() {
if read.is_some_and(|r| r.native_columns.contains_key(column)) {
return Err(refuse("reads a native column, which a document does not hold"));
}
paths.push(crate::utils::to_snake_case(column));
}
let declared = schema.find_type(type_name);
let carried = paths.iter().all(|path| {
declared.is_some_and(|t| {
t.fields.iter().any(|f| crate::utils::to_snake_case(f.name.as_str()) == *path)
})
});
if !carried || !reads_only(&clause, &paths) {
return Err(refuse("reads a key the type does not declare"));
}
let mut conditions = Vec::new();
if !equalities(&clause, &mut conditions) {
return Err(refuse(
"is not a conjunction of equalities, the only shape evaluated over a document",
));
}
Ok(Some(Self { conditions }))
}
pub(in super::super) fn admits(&self, document: &serde_json::Value) -> bool {
meets(document, &self.conditions)
}
}
fn equalities(clause: &WhereClause, out: &mut Vec<DocumentCondition>) -> bool {
match clause {
WhereClause::Field {
path,
operator: crate::backend::WhereOperator::Eq,
value,
} => {
out.push((path.clone(), value.clone()));
true
},
WhereClause::And(all) => all.iter().all(|c| equalities(c, out)),
WhereClause::Typed { inner, .. } => equalities(inner, out),
_ => false,
}
}
fn meets(document: &serde_json::Value, conditions: &[DocumentCondition]) -> bool {
conditions.iter().all(|(path, expected)| {
let actual = path.iter().try_fold(document, |at, key| at.get(key));
!expected.is_null() && actual == Some(expected)
})
}
#[cfg(test)]
#[path = "query_nested_document_tests.rs"]
mod document_tests;