From d3d30aa412b9def00487d9aa6791b3d0d324dbf9 Mon Sep 17 00:00:00 2001 From: Matthew Wright <1815200+matthewguywright@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:44:12 -0500 Subject: [PATCH] MERL-803: overriding introspection (#1594) * initial commit for overriding introspection via WP filter * added test for filter value * test: confirm behavior of `filter_introspection()` * chore: adjust error message in `generatePossibleTypes()` * chore: add changeset --------- Co-authored-by: John Parris Co-authored-by: Jason Bahl --- .changeset/serious-monkeys-shave.md | 6 ++ .../faustwp-cli/src/generatePossibleTypes.ts | 5 +- .../faustwp/includes/graphql/callbacks.php | 30 ++++++++ .../integration/GraphQLCallbacksTests.php | 77 +++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 .changeset/serious-monkeys-shave.md diff --git a/.changeset/serious-monkeys-shave.md b/.changeset/serious-monkeys-shave.md new file mode 100644 index 000000000..7fadd25ad --- /dev/null +++ b/.changeset/serious-monkeys-shave.md @@ -0,0 +1,6 @@ +--- +'@faustwp/cli': minor +'@faustwp/wordpress-plugin': minor +--- + +Added support for authenticated WPGraphQL introspection queries using FAUST_SECRET_KEY. It is no longer required to enable "Public Introspection" in WPGraphQL. diff --git a/packages/faustwp-cli/src/generatePossibleTypes.ts b/packages/faustwp-cli/src/generatePossibleTypes.ts index f7a4ea676..6dcb2aa16 100644 --- a/packages/faustwp-cli/src/generatePossibleTypes.ts +++ b/packages/faustwp-cli/src/generatePossibleTypes.ts @@ -2,7 +2,7 @@ import 'isomorphic-fetch'; import fs from 'fs'; import { infoLog, errorLog, debugLog } from './stdout/index.js'; -import { getGraphqlEndpoint, getWpUrl } from './utils/index.js'; +import { getGraphqlEndpoint, getWpSecret, getWpUrl } from './utils/index.js'; type PossibleTypes = { [key: string]: any; @@ -25,6 +25,7 @@ export async function generatePossibleTypes(): Promise { method: 'POST', headers: { 'Content-Type': 'application/json', + 'x-faust-secret': getWpSecret() || '', }, body: JSON.stringify({ variables: {}, @@ -81,7 +82,7 @@ export async function generatePossibleTypes(): Promise { errorLog("Unable to update this project's possibleTypes schema"); errorLog( - `Make sure you have "Enable Public Introspection" checked in WPGraphQL: ${getWpUrl()}/wp-admin/admin.php?page=graphql-settings`, + `Make sure the FAUST_SECRET_KEY value in your environment matches the value in the Faust WordPress plugin settings, or that you have "Enable Public Introspection" checked in WPGraphQL if not using FAUST_SECRET_KEY: ${getWpUrl()}/wp-admin/admin.php?page=graphql-settings`, ); process.exit(0); diff --git a/plugins/faustwp/includes/graphql/callbacks.php b/plugins/faustwp/includes/graphql/callbacks.php index c9a2b0545..0cc956d8e 100644 --- a/plugins/faustwp/includes/graphql/callbacks.php +++ b/plugins/faustwp/includes/graphql/callbacks.php @@ -8,6 +8,7 @@ namespace WPE\FaustWP\GraphQL; use function WPE\FaustWP\Auth\generate_authorization_code; +use function WPE\FaustWP\Settings\get_secret_key; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; @@ -44,6 +45,35 @@ function register_templates_field() { ); } +add_filter( 'graphql_get_setting_section_field_value', __NAMESPACE__ . '\\filter_introspection', 10, 5 ); +/** + * Enables WPGraphQL public introspection option + * when authenticated requests come from Faust. + * + * @param mixed $value The value of the field. + * @param mixed $default_value The default value if there is no value set. + * @param string $option_name The name of the option. + * @param array $section_fields The setting values within the section. + * @param string $section_name The name of the section the setting belongs to. + */ +function filter_introspection( $value, $default_value, $option_name, $section_fields, $section_name ) { + if ( 'public_introspection_enabled' !== $option_name ) { + return $value; + } + + // check header for faust secret key. + if ( ! isset( $_SERVER['HTTP_X_FAUST_SECRET'] ) ) { + return $value; + }; + + $secret_key = get_secret_key(); + if ( $secret_key !== $_SERVER['HTTP_X_FAUST_SECRET'] ) { + return $value; + } + + return 'on'; +} + add_action( 'graphql_register_types', __NAMESPACE__ . '\\register_faust_toolbar_field' ); /** * Registers a field on the User model called "shouldShowFaustToolbar" which diff --git a/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php b/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php index 158d5ff85..2364fca4a 100644 --- a/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php +++ b/plugins/faustwp/tests/integration/GraphQLCallbacksTests.php @@ -14,6 +14,10 @@ faustwp_update_setting, }; +use function WPE\FaustWP\GraphQL\{ + filter_introspection, +}; + class GraphQLCallbacksTests extends \WP_UnitTestCase { private $graphqlResponse; @@ -107,6 +111,10 @@ public function setUp(): void { $this->graphqlResponse->data = $this->responseData; } + public function test_graphql_section_field_value() { + $this->assertSame( 10, has_action( 'graphql_get_setting_section_field_value', 'WPE\FaustWP\GraphQL\filter_introspection' ) ); + } + public function test_graphql_request_results_filter() { $this->assertSame( 10, has_action( 'graphql_request_results', 'WPE\FaustWP\Replacement\url_replacement' ) ); } @@ -137,4 +145,73 @@ public function test_url_replacement_replaces_url_fields_when_rewrites_are_enabl $filteredRespone = url_replacement( $this->graphqlResponse ); $this->assertSame( $this->expectedData, $filteredRespone->data ); } + + /** + * Tests filter_introspection() does not modify values unrelated to public introspection. + */ + public function test_filter_introspection_returns_same_value_for_unrelated_option_name(): void { + $input = 'leave me alone'; + self::assertSame( + $input, + filter_introspection( $input, false, 'stylesheet', [], 'default' ) + ); + } + + /** + * Tests filter_introspection() does not enable public introspection when the Faust secret key is not present. + */ + public function test_filter_introspection_returns_same_value_when_faust_secret_key_is_not_present(): void { + $input = 'leave me alone'; + self::assertSame( + $input, + filter_introspection( $input, false, 'public_introspection_enabled', [], 'default' ) + ); + } + + /** + * Tests filter_introspection() does not enable public introspection when the Faust secret key is incorrect. + */ + public function test_filter_introspection_returns_same_value_when_faust_secret_key_is_present_but_incorrect(): void { + global $_SERVER; + $_SERVER['HTTP_X_FAUST_SECRET'] = 'wrong-key'; + + $input = 'leave me alone'; + self::assertSame( + $input, + filter_introspection( $input, false, 'public_introspection_enabled', [], 'default' ) + ); + } + + /** + * Tests filter_introspection() enables public introspection when the Faust secret key is correct. + */ + public function test_filter_introspection_returns_on_when_faust_secret_key_is_present_and_correct(): void { + global $_SERVER; + $_SERVER['HTTP_X_FAUST_SECRET'] = 'correct-key'; + + tests_add_filter( 'faustwp_get_setting', [ $this, 'filter_secret_key' ], 10, 3 ); + + $input = 'this should not be returned. "on" should be returned.'; + + self::assertSame( + 'on', + filter_introspection( $input, false, 'public_introspection_enabled', [], 'default' ) + ); + + remove_filter( 'faustwp_get_setting', [ $this, 'filter_secret_key' ] ); + } + + /** + * Filters the secret key value for testing. + * + * @param mixed $value The setting value. + * @param string $name The setting name. + * @param mixed $default Optional setting value. + */ + public function filter_secret_key( $value, $name, $default ) { + if ( 'secret_key' !== $name ) { + return $value; + } + return 'correct-key'; + } }