Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introspection-only mode #894

Merged
merged 1 commit into from Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/context.rs
Expand Up @@ -13,11 +13,11 @@ use http::HeaderValue;
use serde::ser::{SerializeSeq, Serializer};
use serde::Serialize;

use crate::extensions::Extensions;
use crate::parser::types::{
Directive, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
};
use crate::schema::SchemaEnv;
use crate::{extensions::Extensions, schema::IntrospectionMode};
use crate::{
Error, InputType, Lookahead, Name, OneofObjectType, PathSegment, Pos, Positioned, Result,
ServerError, ServerResult, UploadValue, Value,
Expand Down Expand Up @@ -244,7 +244,7 @@ pub struct QueryEnvInner {
pub session_data: Arc<Data>,
pub ctx_data: Arc<Data>,
pub http_headers: Mutex<HeaderMap>,
pub disable_introspection: bool,
pub introspection_mode: IntrospectionMode,
pub errors: Mutex<Vec<ServerError>>,
}

Expand Down
9 changes: 5 additions & 4 deletions src/registry/mod.rs
Expand Up @@ -9,13 +9,14 @@ use indexmap::map::IndexMap;
use indexmap::set::IndexSet;

pub use crate::model::__DirectiveLocation;
use crate::parser::types::{
BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition,
};
use crate::{
model, Any, Context, InputType, OutputType, Positioned, ServerResult, SubscriptionType, Value,
VisitorContext,
};
use crate::{
parser::types::{BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition},
schema::IntrospectionMode,
};

pub use cache_control::CacheControl;

Expand Down Expand Up @@ -401,7 +402,7 @@ pub struct Registry {
pub query_type: String,
pub mutation_type: Option<String>,
pub subscription_type: Option<String>,
pub disable_introspection: bool,
pub introspection_mode: IntrospectionMode,
pub enable_federation: bool,
pub federation_subscription: bool,
}
Expand Down
29 changes: 29 additions & 0 deletions src/request.rs
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize};

use crate::parser::parse_query;
use crate::parser::types::ExecutableDocument;
use crate::schema::IntrospectionMode;
use crate::{Data, ParseRequestError, ServerError, UploadValue, Value, Variables};

/// GraphQL request.
Expand All @@ -14,6 +15,7 @@ use crate::{Data, ParseRequestError, ServerError, UploadValue, Value, Variables}
/// variables. The names are all in `camelCase` (e.g. `operationName`).
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
Copy link
Contributor Author

@cynecx cynecx Apr 18, 2022

Choose a reason for hiding this comment

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

This is actually a breaking change (semver). But #891 has already introduced such breaking change already....

pub struct Request {
/// The query source of the request.
#[serde(default)]
Expand Down Expand Up @@ -42,11 +44,17 @@ pub struct Request {
pub extensions: HashMap<String, Value>,

/// Disable introspection queries for this request.
/// This option has priority over `introspection_mode` when set to true.
/// `introspection_mode` has priority when `disable_introspection` set to `false`.
#[serde(skip)]
pub disable_introspection: bool,

#[serde(skip)]
pub(crate) parsed_query: Option<ExecutableDocument>,

/// Sets the introspection mode for this request (defaults to [IntrospectionMode::Enabled]).
#[serde(skip)]
pub introspection_mode: IntrospectionMode,
}

impl Request {
Expand All @@ -61,6 +69,7 @@ impl Request {
extensions: Default::default(),
disable_introspection: false,
parsed_query: None,
introspection_mode: IntrospectionMode::Enabled,
}
}

Expand Down Expand Up @@ -90,6 +99,15 @@ impl Request {
#[must_use]
pub fn disable_introspection(mut self) -> Self {
self.disable_introspection = true;
self.introspection_mode = IntrospectionMode::Disabled;
self
}

/// Only allow introspection queries for this request.
#[must_use]
pub fn only_introspection(mut self) -> Self {
self.disable_introspection = false;
self.introspection_mode = IntrospectionMode::IntrospectionOnly;
self
}

Expand Down Expand Up @@ -232,6 +250,17 @@ impl BatchRequest {
pub fn disable_introspection(mut self) -> Self {
for request in self.iter_mut() {
request.disable_introspection = true;
request.introspection_mode = IntrospectionMode::Disabled;
}
self
}

/// Only allow introspection queries for each request.
#[must_use]
pub fn introspection_only(mut self) -> Self {
for request in self.iter_mut() {
request.disable_introspection = false;
request.introspection_mode = IntrospectionMode::IntrospectionOnly;
}
self
}
Expand Down
56 changes: 50 additions & 6 deletions src/schema.rs
Expand Up @@ -6,7 +6,6 @@ use std::sync::Arc;
use futures_util::stream::{self, Stream, StreamExt};
use indexmap::map::IndexMap;

use crate::context::{Data, QueryEnvInner};
use crate::custom_directive::CustomDirectiveFactory;
use crate::extensions::{ExtensionFactory, Extensions};
use crate::model::__DirectiveLocation;
Expand All @@ -17,11 +16,29 @@ use crate::resolver_utils::{resolve_container, resolve_container_serial};
use crate::subscription::collect_subscription_streams;
use crate::types::QueryRoot;
use crate::validation::{check_rules, ValidationMode};
use crate::{
context::{Data, QueryEnvInner},
EmptyMutation, EmptySubscription,
};
use crate::{
BatchRequest, BatchResponse, CacheControl, ContextBase, InputType, ObjectType, OutputType,
QueryEnv, Request, Response, ServerError, SubscriptionType, Variables, ID,
};

/// Introspection mode
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum IntrospectionMode {
IntrospectionOnly,
Enabled,
Disabled,
}

impl Default for IntrospectionMode {
fn default() -> Self {
IntrospectionMode::Enabled
}
}

/// Schema builder
pub struct SchemaBuilder<Query, Mutation, Subscription> {
validation_mode: ValidationMode,
Expand Down Expand Up @@ -58,7 +75,14 @@ impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription>
/// Disable introspection queries.
#[must_use]
pub fn disable_introspection(mut self) -> Self {
self.registry.disable_introspection = true;
self.registry.introspection_mode = IntrospectionMode::Disabled;
self
}

/// Only process introspection queries, everything else is processed as an error.
#[must_use]
pub fn introspection_only(mut self) -> Self {
self.registry.introspection_mode = IntrospectionMode::IntrospectionOnly;
self
}

Expand Down Expand Up @@ -303,7 +327,7 @@ where
} else {
Some(Subscription::type_name().to_string())
},
disable_introspection: false,
introspection_mode: IntrospectionMode::Enabled,
enable_federation: false,
federation_subscription: false,
};
Expand Down Expand Up @@ -515,7 +539,11 @@ where
session_data,
ctx_data: query_data,
http_headers: Default::default(),
disable_introspection: request.disable_introspection,
introspection_mode: if request.disable_introspection {
IntrospectionMode::Disabled
} else {
request.introspection_mode
},
errors: Default::default(),
};
Ok((QueryEnv::new(env), validation_result.cache_control))
Expand All @@ -532,7 +560,15 @@ where

let res = match &env.operation.node.ty {
OperationType::Query => resolve_container(&ctx, &self.query).await,
OperationType::Mutation => resolve_container_serial(&ctx, &self.mutation).await,
OperationType::Mutation => {
if self.env.registry.introspection_mode == IntrospectionMode::IntrospectionOnly
|| env.introspection_mode == IntrospectionMode::IntrospectionOnly
{
resolve_container_serial(&ctx, &EmptyMutation).await
} else {
resolve_container_serial(&ctx, &self.mutation).await
}
}
OperationType::Subscription => Err(ServerError::new(
"Subscriptions are not supported on this transport.",
None,
Expand Down Expand Up @@ -627,7 +663,15 @@ where
);

let mut streams = Vec::new();
if let Err(err) = collect_subscription_streams(&ctx, &schema.subscription, &mut streams) {
let collect_result = if schema.env.registry.introspection_mode
== IntrospectionMode::IntrospectionOnly
|| env.introspection_mode == IntrospectionMode::IntrospectionOnly
{
collect_subscription_streams(&ctx, &EmptySubscription, &mut streams)
} else {
collect_subscription_streams(&ctx, &schema.subscription, &mut streams)
};
if let Err(err) = collect_result {
yield Response::from_errors(vec![err]);
}

Expand Down
24 changes: 21 additions & 3 deletions src/types/query_root.rs
Expand Up @@ -2,9 +2,12 @@ use std::borrow::Cow;

use indexmap::map::IndexMap;

use crate::model::{__Schema, __Type};
use crate::parser::types::Field;
use crate::resolver_utils::{resolve_container, ContainerType};
use crate::{
model::{__Schema, __Type},
schema::IntrospectionMode,
};
use crate::{
registry, Any, Context, ContextSelectionSet, ObjectType, OutputType, Positioned, ServerError,
ServerResult, SimpleObject, Value,
Expand All @@ -24,7 +27,13 @@ pub(crate) struct QueryRoot<T> {
#[async_trait::async_trait]
impl<T: ObjectType> ContainerType for QueryRoot<T> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
if !ctx.schema_env.registry.disable_introspection && !ctx.query_env.disable_introspection {
if matches!(
ctx.schema_env.registry.introspection_mode,
IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly
) && matches!(
ctx.query_env.introspection_mode,
IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly,
) {
if ctx.item.node.name.node == "__schema" {
let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set);
let visible_types = ctx.schema_env.registry.find_visible_types(ctx);
Expand Down Expand Up @@ -54,6 +63,12 @@ impl<T: ObjectType> ContainerType for QueryRoot<T> {
}
}

if ctx.schema_env.registry.introspection_mode == IntrospectionMode::IntrospectionOnly
|| ctx.query_env.introspection_mode == IntrospectionMode::IntrospectionOnly
{
return Ok(None);
}

if ctx.schema_env.registry.enable_federation || ctx.schema_env.registry.has_entities() {
if ctx.item.node.name.node == "_entities" {
let (_, representations) = ctx.param_value::<Vec<Any>>("representations", None)?;
Expand Down Expand Up @@ -93,7 +108,10 @@ impl<T: ObjectType> OutputType for QueryRoot<T> {
fn create_type_info(registry: &mut registry::Registry) -> String {
let root = T::create_type_info(registry);

if !registry.disable_introspection {
if matches!(
registry.introspection_mode,
IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly
) {
let schema_type = __Schema::create_type_info(registry);
if let Some(registry::MetaType::Object { fields, .. }) =
registry.types.get_mut(T::type_name().as_ref())
Expand Down
106 changes: 106 additions & 0 deletions tests/introspection.rs
Expand Up @@ -1272,3 +1272,109 @@ pub async fn test_disable_introspection() {
value!({ "__type": null })
);
}

#[tokio::test]
pub async fn test_introspection_only() {
let schema = Schema::build(Query, Mutation, EmptySubscription)
.introspection_only()
.finish();

// Test whether introspection works.
let query = r#"
{
__type(name: "Mutation") {
name
kind
description
fields {
description
name
type { kind name }
args { name }
}
}
}
"#;
let res_json = value!({
"__type": {
"name": "Mutation",
"kind": "OBJECT",
"description": "Global mutation",
"fields": [
{
"description": "simple_mutation description\nline2\nline3",
"name": "simpleMutation",
"type": {
"kind": "NON_NULL",
"name": null
},
"args": [
{
"name": "input"
}
]
}
]
}
});
let res = schema.execute(query).await.into_result().unwrap().data;
assert_eq!(res, res_json);

// Test whether introspection works.
let query = r#"
{
__type(name: "Query") {
name
kind
description
fields {
description
name
type { kind name }
args { name }
}
}
}
"#;
let res_json = value!({
"__type": {
"name": "Query",
"kind": "OBJECT",
"description": "Global query",
"fields": [
{
"description": "Get a simple object",
"name": "simpleObject",
"type": { "kind": "NON_NULL", "name": null },
"args": []
}
]
}
});
let res = schema.execute(query).await.into_result().unwrap().data;
assert_eq!(res, res_json);

// Queries shouldn't work in introspection only mode.
let query = r#"
{
simpleObject {
a
}
}
"#;
let res_json = value!({ "simpleObject": null });
let res = schema.execute(query).await.into_result().unwrap().data;
assert_eq!(res, res_json);

// Mutations shouldn't work in introspection only mode.
let query = r#"
mutation {
simpleMutation(input: { a: "" }) {
a
}
}
"#;
let res_json = value!({ "simpleMutation": null });
let res = schema.execute(query).await.into_result().unwrap().data;
assert_eq!(res, res_json);
}