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

disable introspection plugin #202

Merged
merged 2 commits into from
Dec 18, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions libs/common/src/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,72 @@ impl ParsedGraphQLRequest {
})
}

pub fn executable_operation(&self) -> Option<&Definition<'static, String>> {
match &self.request.operation_name {
Some(op_name) => self.parsed_operation.definitions.iter().find(|v| {
if let Definition::Operation(op) = v {
let name: &Option<String> = match op {
OperationDefinition::SelectionSet(_) => &None,
OperationDefinition::Query(query) => &query.name,
OperationDefinition::Mutation(mutation) => &mutation.name,
OperationDefinition::Subscription(subscription) => &subscription.name,
};

if let Some(actual_name) = name {
return actual_name == op_name;
}
}

false
}),
_ => self.parsed_operation.definitions.iter().find(|v| {
if let Definition::Operation(_) = v {
return true;
}

false
}),
}
}

pub fn is_introspection_query(&self) -> bool {
let operation_to_execute = self.executable_operation();
let root_level_selections = match operation_to_execute {
Some(Definition::Operation(OperationDefinition::SelectionSet(s))) => Some(s),
Some(Definition::Operation(OperationDefinition::Query(q))) => Some(&q.selection_set),
_ => None,
};

if let Some(selections) = root_level_selections {
let all_typename = selections.items.iter().all(|v| {
// TODO: Should we handle Fragments here as well?
if let graphql_parser::query::Selection::Field(field) = v {
return field.name == "__typename";
}

false
});

if all_typename {
return true;
}

let has_some_introspection_fields = selections.items.iter().any(|v| {
if let graphql_parser::query::Selection::Field(field) = v {
return field.name == "__schema" || field.name == "__type";
}

false
});

if has_some_introspection_fields {
return true;
}
}

false
}

#[tracing::instrument(level = "trace", name = "ParsedGraphQLRequest::is_running_mutation")]
pub fn is_running_mutation(&self) -> bool {
if let Some(operation_name) = &self.request.operation_name {
Expand Down
202 changes: 138 additions & 64 deletions libs/config/conductor.schema.json

Large diffs are not rendered by default.

24 changes: 1 addition & 23 deletions libs/config/src/generate-json-schema.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,11 @@
use conductor_config::ConductorConfig;
use schemars::{
gen::SchemaSettings,
schema::SchemaObject,
visit::{visit_schema_object, Visitor},
};

#[derive(Debug, Clone)]
pub struct PatchDescriptionVisitor;

impl Visitor for PatchDescriptionVisitor {
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
if let Some(desc) = schema
.metadata
.as_mut()
.and_then(|m| m.description.as_mut())
{
*desc = desc.replace("\n\n", "\n");
}

visit_schema_object(self, schema);
}
}
use schemars::gen::SchemaSettings;

pub fn main() {
println!("⚙️ Generating JSON schema for Conductor config file...");
// Please keep this 2019/09, see https://github.com/GREsau/schemars/issues/42#issuecomment-642603632
// Website documentation generator depends on this.
let schema = SchemaSettings::draft2019_09()
.with_visitor(PatchDescriptionVisitor {})
.into_generator()
.into_root_schema_for::<ConductorConfig>();
let as_string = serde_json::to_string_pretty(&schema).unwrap();
Expand Down
16 changes: 14 additions & 2 deletions libs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pub mod serde_utils;

use interpolate::interpolate;
use plugins::{
CorsPluginConfig, GraphiQLPluginConfig, HttpGetPluginConfig, PersistedOperationsPluginConfig,
PersistedOperationsProtocolConfig, VrlPluginConfig,
CorsPluginConfig, DisableIntrospectionPluginConfig, GraphiQLPluginConfig, HttpGetPluginConfig,
PersistedOperationsPluginConfig, PersistedOperationsProtocolConfig, VrlPluginConfig,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -231,6 +231,18 @@ pub enum PluginDefinition {
config: Option<CorsPluginConfig>,
},

#[serde(rename = "disable_introspection")]
/// Configuration for the Disable Introspection plugin.
DisableItrospectionPlugin {
#[serde(
default = "default_plugin_enabled",
skip_serializing_if = "Option::is_none"
)]
enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<DisableIntrospectionPluginConfig>,
},

#[serde(rename = "http_get")]
HttpGetPlugin {
#[serde(
Expand Down
82 changes: 81 additions & 1 deletion libs/config/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,68 @@ use crate::{
PluginDefinition,
};

#[derive(Default, Deserialize, Serialize, Debug, Clone, JsonSchema)]
#[schemars(example = "disable_introspection_example1")]
#[schemars(example = "disable_introspection_example2")]
/// The `disable_introspection` plugin allows you to disable introspection for your GraphQL API.
///
/// A [GraphQL introspection query](https://graphql.org/learn/introspection/) is a special GraphQL query that returns information about the GraphQL schema of your API.
///
/// It it [recommended to disable introspection for production environments](https://escape.tech/blog/should-i-disable-introspection-in-graphql/), unless you have a specific use-case for it.
///
/// It can either disable introspection for all requests, or only for requests that match a specific condition (using VRL scripting language).
///
pub struct DisableIntrospectionPluginConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
/// A VRL condition that determines whether to disable introspection for the request. This condition is evaluated only if the incoming GraphQL request is detected as an introspection query.
///
/// The condition is evaluated in the context of the incoming request and have access to the metadata field `%downstream_http_req` (fields: `body`, `uri`, `query_string`, `method`, `headers`).
///
/// The condition must return a boolean value: return `true` to continue and disable the introspection, and `false` to allow the introspection to run.
///
/// In case of a runtime error, or an unexpected return value, the script will be ignored and introspection will be disabled for the incoming request.
pub condition: Option<VrlConfigReference>,
}

fn disable_introspection_example1() -> JsonSchemaExample<PluginDefinition> {
JsonSchemaExample {
metadata: JsonSchemaExampleMetadata::new(
"Disable Introspection",
Some(
"This example disables introspection for all requests for the configured Endpoint.",
),
),
example: PluginDefinition::DisableItrospectionPlugin {
enabled: Some(true),
config: Some(DisableIntrospectionPluginConfig {
..Default::default()
}),
},
}
}

fn disable_introspection_example2() -> JsonSchemaExample<PluginDefinition> {
JsonSchemaExample {
metadata: JsonSchemaExampleMetadata::new(
"Conditional",
Some(
"This example disables introspection for all requests that doesn't have the \"bypass-introspection\" HTTP header.",
),
),
example: PluginDefinition::DisableItrospectionPlugin {
enabled: Some(true),
config: Some(DisableIntrospectionPluginConfig {
condition: Some(VrlConfigReference::Inline { content: "%downstream_http_req.headers.\"bypass-introspection\" != \"1\"".to_string() }),
}),
},
}
}

/// The `cors` plugin enables [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) configuration for your GraphQL API.
///
/// By using this plugin, you can define rules for allowing cross-origin requests to your GraphQL server. This is essential for web applications that need to interact with your API from different domains.
///
#[derive(Default, Deserialize, Serialize, Debug, Clone, JsonSchema)]
#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)]
#[schemars(example = "cors_plugin_example1")]
#[schemars(example = "cors_plugin_example2")]
#[schemars(example = "cors_plugin_example3")]
Expand Down Expand Up @@ -59,6 +116,20 @@ pub struct CorsPluginConfig {
pub max_age: Option<u64>,
}

impl Default for CorsPluginConfig {
fn default() -> Self {
Self {
allow_credentials: default_boolean_false(),
allowed_methods: default_wildcard(),
allowed_origin: default_wildcard(),
allowed_headers: default_wildcard(),
exposed_headers: default_wildcard(),
allow_private_network: default_boolean_false(),
max_age: defualt_max_age(),
}
}
}

fn default_wildcard() -> Option<String> {
Some("*".to_string())
}
Expand Down Expand Up @@ -776,3 +847,12 @@ pub enum VrlConfigReference {
/// File reference to a VRL file. The file is loaded and executed as a VRL plugin.
File { path: LocalFileReference },
}

impl VrlConfigReference {
pub fn contents(&self) -> &String {
match self {
VrlConfigReference::Inline { content } => content,
VrlConfigReference::File { path } => &path.contents,
}
}
}
Loading
Loading