diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 075041273..c1d4bb73c 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -34,7 +34,7 @@ App::get('/v1/graphql') ->inject('register') ->inject('dbForProject') ->inject('promiseAdapter') - ->inject('gqlSchema') + ->inject('schema') ->action(Closure::fromCallable('graphqlRequest')); App::post('/v1/graphql') @@ -54,7 +54,7 @@ App::post('/v1/graphql') ->inject('request') ->inject('response') ->inject('promiseAdapter') - ->inject('gqlSchema') + ->inject('schema') ->action(Closure::fromCallable('graphqlRequest')); /** @@ -66,7 +66,7 @@ function graphqlRequest( Appwrite\Utopia\Request $request, Appwrite\Utopia\Response $response, CoroutinePromiseAdapter $promiseAdapter, - Type\Schema $gqlSchema + Type\Schema $schema ): void { $contentType = $request->getHeader('content-type'); $maxBatchSize = App::getEnv('_APP_GRAPHQL_MAX_BATCH_SIZE', 50); @@ -108,7 +108,7 @@ function graphqlRequest( foreach ($query as $indexed) { $promises[] = GraphQL::promiseToExecute( $promiseAdapter, - $gqlSchema, + $schema, $indexed['query'], variableValues: $indexed['variables'] ?? null, operationName: $indexed['operationName'] ?? null, @@ -121,12 +121,11 @@ function graphqlRequest( $wg->add(); $promiseAdapter->all($promises)->then( function (array $results) use (&$output, &$wg, $debugFlags) { - processResult($results, $output, $debugFlags); - $wg->done(); - }, - function ($error) use (&$output, $wg) { - $output = ['errors' => [$error]]; - $wg->done(); + try { + processResult($results, $output, $debugFlags); + } finally { + $wg->done(); + } } ); $wg->wait(); diff --git a/app/init.php b/app/init.php index d136ade5e..c92702899 100644 --- a/app/init.php +++ b/app/init.php @@ -1006,6 +1006,6 @@ App::setResource('promiseAdapter', function ($register) { return $register->get('promiseAdapter'); }, ['register']); -App::setResource('gqlSchema', function ($utopia, $dbForProject) { +App::setResource('schema', function ($utopia, $dbForProject) { return SchemaBuilder::buildSchema($utopia, $dbForProject); }, ['utopia', 'dbForProject']); diff --git a/tests/e2e/Services/GraphQL/GraphQLAbuseTest.php b/tests/e2e/Services/GraphQL/GraphQLAbuseTest.php new file mode 100644 index 000000000..6dc4b9308 --- /dev/null +++ b/tests/e2e/Services/GraphQL/GraphQLAbuseTest.php @@ -0,0 +1,68 @@ +getProject()['$id']; + $query = $this->getQuery(self::$COMPLEX_QUERY); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => 'user', + 'email' => 'user@appwrite.io', + 'password' => 'password', + 'databaseId' => 'database', + 'databaseName' => 'database', + 'collectionId' => 'collection', + 'collectionName' => 'collection', + 'collectionPermission' => 'collection', + 'collectionRead' => ['role:member'], + 'collectionWrite' => ['role:member'], + 'documentId' => 'document', + 'documentData' => ['name' => 'foobar'], + 'documentRead' => ['role:member'], + 'documentWrite' => ['role:member'], + ], + ]; + + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + \var_dump($response); + + $this->assertEquals('Too many queries.', $response['body']['message']); + } + + public function testTooManyQueriesBlocked() + { + $projectId = $this->getProject()['$id']; + $maxQueries = App::getEnv('_APP_GRAPHQL_MAX_QUERIES', 50); + + $query = []; + for ($i = 0; $i <= $maxQueries + 1; $i++) { + $query[] = ['query' => $this->getQuery(self::$LIST_COUNTRIES)]; + } + + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $query); + + $this->assertEquals('Too many queries.', $response['body']['message']); + } +} diff --git a/tests/e2e/Services/GraphQL/GraphQLBase.php b/tests/e2e/Services/GraphQL/GraphQLBase.php index de444169a..fa5f5198d 100644 --- a/tests/e2e/Services/GraphQL/GraphQLBase.php +++ b/tests/e2e/Services/GraphQL/GraphQLBase.php @@ -174,6 +174,8 @@ trait GraphQLBase public static string $GET_QRCODE = 'get_qrcode'; public static string $GET_USER_INITIALS = 'get_user_initials'; + public static string $COMPLEX_QUERY = 'complex_query'; + public function getQuery(string $name): string { switch ($name) { @@ -1346,6 +1348,18 @@ trait GraphQLBase status } }'; + case self::$COMPLEX_QUERY: + return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $collectionPermission: String!, $collectionRead: [String!]!, $collectionWrite: [String!]!) { + databasesCreate(databaseId: $databaseId, name: $databaseName) { + _id + name + } + databasesCreateCollection(databaseId: $databaseId, collectionId: $collectionId, name: $collectionName, permission: $collectionPermission, read: $collectionRead, write: $collectionWrite) { + _id + name + permission + } + }'; } throw new \InvalidArgumentException('Invalid query type'); diff --git a/tests/e2e/Services/GraphQL/GraphQLContentTypeTest.php b/tests/e2e/Services/GraphQL/GraphQLContentTypeTest.php index bde4f3557..4168d22b2 100644 --- a/tests/e2e/Services/GraphQL/GraphQLContentTypeTest.php +++ b/tests/e2e/Services/GraphQL/GraphQLContentTypeTest.php @@ -46,7 +46,7 @@ class GraphQLContentTypeTest extends Scope $this->assertEquals(194, $response['total']); } - public function testBatchedJSONContentType() + public function testArrayBatchedJSONContentType() { $projectId = $this->getProject()['$id']; $query1 = 'query { localeGetCountries { total countries { code } } }'; @@ -68,6 +68,31 @@ class GraphQLContentTypeTest extends Scope $this->assertEquals(7, $response['body']['data']['localeGetContinents']['total']); } + public function testQueryBatchedJSONContentType() + { + $projectId = $this->getProject()['$id']; + $query = ' + query { + localeGetCountries { total countries { code } } + localeGetContinents { total continents { code } } + } + '; + $graphQLPayload = [ + ['query' => $query], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertIsArray($response['body']['data']); + $this->assertArrayNotHasKey('errors', $response['body']); + $this->assertArrayHasKey('localeGetCountries', $response['body']['data']); + $this->assertArrayHasKey('localeGetContinents', $response['body']['data']); + $this->assertEquals(194, $response['body']['data']['localeGetCountries']['total']); + $this->assertEquals(7, $response['body']['data']['localeGetContinents']['total']); + } + public function testMultipartFormDataContentType() { $projectId = $this->getProject()['$id'];