use std::borrow::Cow;
use super::{
super::context::ExecutorContext,
query_nested::{DocumentRowFilter, LevelAuthz, SelectionAccess},
};
use crate::{
error::{FraiseQLError, Result},
graphql::FieldSelection,
runtime::project_entity,
security::SecurityContext,
};
pub(in super::super) struct ReadPlan {
access: SelectionAccess,
rows: DocumentRowFilter,
}
impl ReadPlan {
pub(in super::super) fn classify(
ctx: &ExecutorContext,
security_ctx: Option<&SecurityContext>,
variables: Option<&serde_json::Value>,
roots: &[(&str, &[FieldSelection])],
) -> Result<Self> {
let access = SelectionAccess::classify_roots(
&ctx.schema,
roots,
security_ctx,
LevelAuthz::from_config(&ctx.config, variables),
)?;
let mut rows = DocumentRowFilter::default();
for (type_name, sels) in roots {
rows.plan(ctx, type_name, sels, security_ctx)?;
}
let unbound = std::collections::HashMap::new();
for (type_name, sels) in roots {
field_authz_inputs(ctx, security_ctx, type_name, sels, &unbound)?;
}
Ok(Self { access, rows })
}
pub(in super::super) fn serve(
&self,
ctx: &ExecutorContext,
security_ctx: Option<&SecurityContext>,
type_name: &str,
selections: &[FieldSelection],
document: &serde_json::Value,
variables: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value> {
let mut source = Cow::Borrowed(document);
if !self.rows.is_empty() {
self.rows.apply(source.to_mut(), type_name, selections, &ctx.schema);
}
let mut projected = project_entity(&source, type_name, selections, &ctx.schema);
self.access.null_masked(&mut projected, type_name, selections, &ctx.schema);
let masked = self.access.masked_fields(type_name);
enforce_field_authz(
ctx,
security_ctx,
type_name,
selections,
&source,
&mut projected,
variables,
&masked,
)?;
Ok(projected)
}
}
struct FieldAuthzInputs<'a> {
principal: &'a SecurityContext,
authorizer: &'a dyn crate::security::FieldAuthorizer,
gated: Vec<crate::security::field_authorizer::GatedField>,
}
fn field_authz_inputs<'a>(
ctx: &'a ExecutorContext,
security_ctx: Option<&'a SecurityContext>,
type_name: &str,
selections: &[FieldSelection],
variables: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<Option<FieldAuthzInputs<'a>>> {
use crate::security::field_authorizer as authz;
if !authz::selection_set_selects_gated_field(&ctx.schema, type_name, selections) {
return Ok(None);
}
let Some(principal) = security_ctx else {
return Err(FraiseQLError::Authorization {
message: format!(
"Field-level authorization is required for a selected field on type \
'{type_name}' but the request is not authenticated"
),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
});
};
let Some(authorizer) = ctx.config.field_authorizer.as_ref() else {
return Err(FraiseQLError::Authorization {
message: format!(
"Field-level authorization is required for a selected field on type \
'{type_name}' but no field authorizer is configured"
),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
});
};
if authz::selection_set_has_nested_gated_field(&ctx.schema, type_name, selections) {
return Err(FraiseQLError::Authorization {
message: format!(
"Field-level authorization of nested fields on type '{type_name}' is not \
supported in this version"
),
action: Some("read".to_string()),
resource: Some(type_name.to_string()),
});
}
let gated =
authz::collect_top_level_gated_fields(&ctx.schema, type_name, selections, variables)?;
Ok(Some(FieldAuthzInputs {
principal,
authorizer: authorizer.as_ref(),
gated,
}))
}
#[allow(clippy::too_many_arguments)]
fn enforce_field_authz(
ctx: &ExecutorContext,
security_ctx: Option<&SecurityContext>,
type_name: &str,
selections: &[FieldSelection],
entity: &serde_json::Value,
projected: &mut serde_json::Value,
variables: &std::collections::HashMap<String, serde_json::Value>,
statically_masked: &[String],
) -> Result<()> {
let Some(inputs) = field_authz_inputs(ctx, security_ctx, type_name, selections, variables)?
else {
return Ok(());
};
let pass = crate::security::field_authorizer::FieldAuthzPass {
authorizer: inputs.authorizer,
principal: inputs.principal,
type_name,
gated: &inputs.gated,
statically_masked,
};
crate::security::field_authorizer::apply_field_authorizer_to_entity(&pass, entity, projected)
}