Merge pull request #894 from cynecx/introspection_mode2

Introspection-only mode
This commit is contained in:
Sunli 2022-04-19 10:25:42 +08:00 committed by GitHub
commit cb4fe27999
6 changed files with 213 additions and 15 deletions

View File

@ -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,
@ -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>>,
}

View File

@ -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;
@ -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,
}

View File

@ -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.
@ -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]
pub struct Request {
/// The query source of the request.
#[serde(default)]
@ -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 {
@ -61,6 +69,7 @@ impl Request {
extensions: Default::default(),
disable_introspection: false,
parsed_query: None,
introspection_mode: IntrospectionMode::Enabled,
}
}
@ -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
}
@ -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
}

View File

@ -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;
@ -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,
@ -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
}
@ -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,
};
@ -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))
@ -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,
@ -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]);
}

View File

@ -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,
@ -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);
@ -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)?;
@ -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())

View File

@ -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);
}