Skip to content

Commit

Permalink
feat(jit): plan ahead auth requirements when required (#3139)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
karatakis and tusharmath authored Nov 23, 2024
1 parent 847d1da commit 4e0979c
Show file tree
Hide file tree
Showing 26 changed files with 228 additions and 120 deletions.
32 changes: 22 additions & 10 deletions src/core/jit/exec_const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ use tailcall_valid::Validator;

use super::context::Context;
use super::exec::{Executor, IRExecutor};
use super::{
transform, AnyResponse, BuildError, Error, OperationPlan, Pos, Positioned, Request, Response,
Result,
};
use super::graphql_error::GraphQLError;
use super::{transform, AnyResponse, BuildError, Error, OperationPlan, Request, Response, Result};
use crate::core::app_context::AppContext;
use crate::core::http::RequestContext;
use crate::core::ir::model::IR;
use crate::core::ir::{self, EvalContext};
use crate::core::ir::{self, EmptyResolverContext, EvalContext};
use crate::core::jit::synth::Synth;
use crate::core::jit::transform::InputResolver;
use crate::core::json::{JsonLike, JsonLikeList};
Expand Down Expand Up @@ -42,6 +40,20 @@ impl ConstValueExecutor {
req_ctx: &RequestContext,
request: Request<ConstValue>,
) -> AnyResponse<Vec<u8>> {
// Run all the IRs in the before chain
if let Some(ir) = &self.plan.before {
let mut eval_context = EvalContext::new(req_ctx, &EmptyResolverContext {});
match ir.eval(&mut eval_context).await {
Ok(_) => (),
Err(err) => {
let resp: Response<ConstValue> = Response::default();
return resp
.with_errors(vec![GraphQLError::new(err.to_string(), None)])
.into();
}
}
}

let is_introspection_query =
req_ctx.server.get_enable_introspection() && self.plan.is_introspection_query;
let variables = &request.variables;
Expand All @@ -54,7 +66,7 @@ impl ConstValueExecutor {
let resp: Response<ConstValue> = Response::default();
// this shouldn't actually ever happen
return resp
.with_errors(vec![Positioned::new(Error::Unknown, Pos::default())])
.with_errors(vec![GraphQLError::new(Error::Unknown.to_string(), None)])
.into();
};

Expand All @@ -69,9 +81,9 @@ impl ConstValueExecutor {
Err(err) => {
let resp: Response<ConstValue> = Response::default();
return resp
.with_errors(vec![Positioned::new(
BuildError::from(err).into(),
Pos::default(),
.with_errors(vec![GraphQLError::new(
BuildError::from(err).to_string(),
None,
)])
.into();
}
Expand Down Expand Up @@ -128,7 +140,7 @@ impl<'a> ConstValueExec<'a> {
}
}

impl<'ctx> IRExecutor for ConstValueExec<'ctx> {
impl IRExecutor for ConstValueExec<'_> {
type Input = ConstValue;
type Output = ConstValue;
type Error = Error;
Expand Down
11 changes: 8 additions & 3 deletions src/core/jit/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ pub struct OperationPlan<Input> {
pub is_protected: bool,
pub min_cache_ttl: Option<NonZeroU64>,
pub selection: Vec<Field<Input>>,

/// An IR that should be executed before the operation starts executing
pub before: Option<IR>,
}

impl<Input> std::fmt::Debug for OperationPlan<Input> {
Expand All @@ -312,15 +315,16 @@ impl<Input> OperationPlan<Input> {
}

Ok(OperationPlan {
selection,
root_name: self.root_name,
operation_type: self.operation_type,
selection,
index: self.index,
is_introspection_query: self.is_introspection_query,
is_dedupe: self.is_dedupe,
is_const: self.is_const,
min_cache_ttl: self.min_cache_ttl,
is_protected: self.is_protected,
min_cache_ttl: self.min_cache_ttl,
before: self.before,
})
}
}
Expand All @@ -345,8 +349,9 @@ impl<Input> OperationPlan<Input> {
is_introspection_query,
is_dedupe: false,
is_const: false,
min_cache_ttl: None,
is_protected: false,
min_cache_ttl: None,
before: Default::default(),
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/core/jit/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ impl Request<ConstValue> {
let plan = builder.build(self.operation_name.as_deref())?;

transform::CheckConst::new()
.pipe(transform::CheckDedupe::new())
.pipe(transform::CheckProtected::new())
.pipe(transform::AuthPlaner::new().when(blueprint.server.auth.is_some()))
.pipe(transform::CheckDedupe::new())
.pipe(transform::CheckCache::new())
.transform(plan)
.to_result()
Expand Down
86 changes: 86 additions & 0 deletions src/core/jit/transform/auth_planer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::convert::Infallible;

use tailcall_valid::Valid;

use crate::core::blueprint::DynamicValue;
use crate::core::ir::model::IR;
use crate::core::jit::{Field, OperationPlan};
use crate::core::Transform;

pub struct AuthPlaner<A> {
marker: std::marker::PhantomData<A>,
}

impl<A> AuthPlaner<A> {
pub fn new() -> Self {
Self { marker: std::marker::PhantomData }
}
}

impl<A> Transform for AuthPlaner<A> {
type Value = OperationPlan<A>;
type Error = Infallible;

fn transform(&self, mut plan: Self::Value) -> Valid<Self::Value, Self::Error> {
let mut before = Vec::new();

plan.selection = plan
.selection
.into_iter()
.map(|field| drop_protect_field(&mut before, field))
.collect();
let ir = before.into_iter();

plan.before = match plan.before {
Some(before) => Some(ir.fold(before, |a, b| a.pipe(b))),
None => ir.reduce(|a, b| a.pipe(b)),
};
Valid::succeed(plan)
}
}

/// Used to recursively update the field ands its selections to remove
/// IR::Protected
fn drop_protect_field<A>(before: &mut Vec<IR>, mut field: Field<A>) -> Field<A> {
if let Some(mut ir) = field.ir {
let is_protected = drop_protect_ir(&mut ir);

field.selection = field
.selection
.into_iter()
.map(|field| drop_protect_field(before, field))
.collect();

if is_protected {
let ir = IR::Protect(Box::new(IR::Dynamic(DynamicValue::Value(
Default::default(),
))));
before.push(ir);
}

field.ir = Some(ir);
}
field
}

/// This function modifies an IR chain by detecting and removing any
/// instances of IR::Protect from the chain. Returns `true` when it modifies the
/// IR.
pub fn drop_protect_ir(ir: &mut IR) -> bool {
match ir {
IR::Dynamic(_) => false,
IR::IO(_) => false,
IR::Cache(_) => false,
IR::Path(ir, _) => drop_protect_ir(ir),
IR::ContextPath(_) => false,
IR::Protect(inner_ir) => {
*ir = *inner_ir.clone();
true
}
IR::Map(_) => false,
IR::Pipe(ir1, ir2) => drop_protect_ir(ir1) || drop_protect_ir(ir2),
IR::Discriminate(_, ir) => drop_protect_ir(ir),
IR::Entity(_) => false,
IR::Service(_) => false,
}
}
1 change: 1 addition & 0 deletions src/core/jit/transform/input_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ where
is_protected: self.plan.is_protected,
min_cache_ttl: self.plan.min_cache_ttl,
selection,
before: self.plan.before,
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/jit/transform/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod auth_planer;
mod check_cache;
mod check_const;
mod check_dedupe;
mod check_protected;
mod input_resolver;
mod skip;

pub use auth_planer::*;
pub use check_cache::*;
pub use check_const::*;
pub use check_dedupe::*;
Expand Down
2 changes: 1 addition & 1 deletion src/core/rest/partial_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct PartialRequest<'a> {
pub path: &'a Path,
}

impl<'a> PartialRequest<'a> {
impl PartialRequest<'_> {
pub async fn into_request(self, request: Request) -> Result<GraphQLRequest> {
let mut variables = self.variables;
if let Some(key) = self.body {
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-basic.md_1.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Missing Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Missing Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-basic.md_2.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Invalid Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Invalid Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-basic.md_4.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Missing Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Missing Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-basic.md_6.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Invalid Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Invalid Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-jwt.md_1.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Missing Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Missing Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-jwt.md_3.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Missing Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Missing Authorization Header"
}
]
}
Expand Down
9 changes: 2 additions & 7 deletions tests/core/snapshots/auth-jwt.md_5.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: tests/core/spec.rs
expression: response
snapshot_kind: text
---
{
"status": 200,
Expand All @@ -11,13 +12,7 @@ expression: response
"data": null,
"errors": [
{
"message": "Authentication Failure: Invalid Authorization Header",
"locations": [
{
"line": 2,
"column": 3
}
]
"message": "Authentication Failure: Invalid Authorization Header"
}
]
}
Expand Down
Loading

1 comment on commit 4e0979c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 4.06ms 2.00ms 42.91ms 81.14%
Req/Sec 6.33k 822.38 7.27k 95.25%

755631 requests in 30.01s, 3.79GB read

Requests/sec: 25177.45

Transfer/sec: 129.23MB

Please sign in to comment.