Skip to content

Commit 17cc08f

Browse files
authored
Provide permissions for each server separately. (drupal-graphql#840)
1 parent 290e4e2 commit 17cc08f

11 files changed

+201
-43
lines changed

graphql.permissions.yml

+10-17
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
'execute graphql requests':
2-
title: 'Execute arbitrary GraphQL requests'
3-
description: 'Allows users to execute arbitrary GraphQL requests.'
4-
5-
'execute persisted graphql requests':
6-
title: 'Execute persisted GraphQL requests'
7-
description: 'Allows users to execute persisted GraphQL requests.'
8-
9-
'use graphql explorer':
10-
title: 'Use GraphiQL'
11-
description: 'Allows users to use the GraphiQL interface.'
12-
13-
'use graphql voyager':
14-
title: 'Use GraphQL Voyager'
15-
description: 'Allows users to use the GraphQL Voyager interface.'
16-
17-
'administer graphql configuration':
1+
administer graphql configuration:
182
title: 'Administer configuration'
193
description: 'Allows users to create, edit and delete server configurations.'
4+
restrict access: true
5+
6+
bypass graphql access:
7+
title: 'Bypass graphql access restrictions'
8+
description: 'Use any graphql server regardless of permission restrictions.'
9+
restrict access: true
10+
11+
permission_callbacks:
12+
- graphql.permission_provider::permissions

graphql.routing.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ graphql.explorer:
3434
_controller: '\Drupal\graphql\Controller\ExplorerController::viewExplorer'
3535
_title: 'Explorer'
3636
requirements:
37-
_permission: 'use graphql explorer'
37+
_graphql_explorer_access: graphql_server:{graphql_server}
3838
options:
3939
_admin_route: TRUE
4040
parameters:
41-
graphql_server:
41+
server:
4242
type: entity:graphql_server
4343

4444
graphql.voyager:
@@ -47,7 +47,7 @@ graphql.voyager:
4747
_controller: '\Drupal\graphql\Controller\VoyagerController::viewVoyager'
4848
_title: 'Voyager'
4949
requirements:
50-
_permission: 'use graphql voyager'
50+
_graphql_voyager_access: graphql_server:{graphql_server}
5151
options:
5252
_admin_route: TRUE
5353
parameters:
@@ -65,4 +65,4 @@ entity.graphql_server.delete_form:
6565
_admin_route: TRUE
6666

6767
route_callbacks:
68-
- 'graphql.route_provider::routes'
68+
- graphql.route_provider::routes

graphql.services.yml

+15-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ services:
1818
arguments: ['@request_stack']
1919
tags:
2020
- { name: access_check, applies_to: _graphql_query_access }
21+
access_check.graphql.explorer:
22+
class: Drupal\graphql\Access\ExplorerAccessCheck
23+
tags:
24+
- { name: access_check, applies_to: _graphql_explorer_access }
25+
access_check.graphql.voyager:
26+
class: Drupal\graphql\Access\VoyagerAccessCheck
27+
tags:
28+
- { name: access_check, applies_to: _graphql_voyager_access }
2129

2230
# Logger channel for graphql related logging.
2331
logger.channel.graphql:
@@ -69,8 +77,13 @@ services:
6977

7078
# Handles the dynamic creation of routes (see graphql.routing.yml).
7179
graphql.route_provider:
72-
class: Drupal\graphql\Routing\QueryRouteProvider
73-
arguments: ['@authentication_collector']
80+
class: Drupal\graphql\RouteProvider
81+
arguments: ['@entity_type.manager', '@authentication_collector']
82+
83+
# Handles the dynamic creation of routes (see graphql.permissions.yml).
84+
graphql.permission_provider:
85+
class: Drupal\graphql\PermissionProvider
86+
arguments: ['@entity_type.manager']
7487

7588
# Schema introspection service.
7689
graphql.introspection:

src/Access/ExplorerAccessCheck.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Access;
4+
5+
use Drupal\Core\Access\AccessResult;
6+
use Drupal\Core\Routing\Access\AccessInterface;
7+
use Drupal\Core\Session\AccountInterface;
8+
use Drupal\graphql\Entity\ServerInterface;
9+
10+
class ExplorerAccessCheck implements AccessInterface {
11+
12+
/**
13+
* Checks access.
14+
*
15+
* @param \Drupal\Core\Session\AccountInterface $account
16+
* The currently logged in account.
17+
* @param \Drupal\graphql\Entity\ServerInterface $graphql_server
18+
* The server instance.
19+
*
20+
* @return \Drupal\Core\Access\AccessResultInterface
21+
* The access result.
22+
*/
23+
public function access(AccountInterface $account, ServerInterface $graphql_server) {
24+
if ($account->hasPermission('bypass graphql access')) {
25+
return AccessResult::allowed();
26+
}
27+
28+
$id = $graphql_server->id();
29+
return AccessResult::allowedIfHasPermissions($account, [
30+
"use $id graphql explorer",
31+
"execute $id arbitrary graphql requests",
32+
]);
33+
}
34+
35+
}

src/Access/QueryAccessCheck.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Drupal\Core\Access\AccessResult;
66
use Drupal\Core\Routing\Access\AccessInterface;
77
use Drupal\Core\Session\AccountInterface;
8+
use Drupal\graphql\Entity\ServerInterface;
89
use Symfony\Component\HttpFoundation\RequestStack;
910

1011
class QueryAccessCheck implements AccessInterface {
@@ -31,13 +32,20 @@ public function __construct(RequestStack $requestStack) {
3132
*
3233
* @param \Drupal\Core\Session\AccountInterface $account
3334
* The currently logged in account.
35+
* @param \Drupal\graphql\Entity\ServerInterface $graphql_server
36+
* The server instance.
3437
*
3538
* @return \Drupal\Core\Access\AccessResultInterface
3639
* The access result.
3740
*/
38-
public function access(AccountInterface $account) {
41+
public function access(AccountInterface $account, ServerInterface $graphql_server) {
42+
if ($account->hasPermission('bypass graphql access')) {
43+
return AccessResult::allowed();
44+
}
45+
46+
$id = $graphql_server->id();
3947
// If the user has the global permission to execute any query, let them.
40-
if ($account->hasPermission('execute graphql requests')) {
48+
if ($account->hasPermission("execute $id arbitrary graphql requests")) {
4149
return AccessResult::allowed();
4250
}
4351

@@ -53,12 +61,12 @@ public function access(AccountInterface $account) {
5361
// not a persisted query). Hence, we only grant access if the user has the
5462
// permission to execute any query.
5563
if ($operation->getOriginalInput('query')) {
56-
return AccessResult::allowedIfHasPermission($account, 'execute graphql requests');
64+
return AccessResult::allowedIfHasPermission($account, "execute $id arbitrary graphql requests");
5765
}
5866
}
5967

6068
// If we reach this point, this is a persisted query.
61-
return AccessResult::allowedIfHasPermission($account, 'execute persisted graphql requests');
69+
return AccessResult::allowedIfHasPermission($account, "execute $id persisted graphql requests");
6270
}
6371

6472
}

src/Access/VoyagerAccessCheck.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Drupal\graphql\Access;
4+
5+
use Drupal\Core\Access\AccessResult;
6+
use Drupal\Core\Routing\Access\AccessInterface;
7+
use Drupal\Core\Session\AccountInterface;
8+
use Drupal\graphql\Entity\ServerInterface;
9+
10+
class VoyagerAccessCheck implements AccessInterface {
11+
12+
/**
13+
* Checks access.
14+
*
15+
* @param \Drupal\Core\Session\AccountInterface $account
16+
* The currently logged in account.
17+
* @param \Drupal\graphql\Entity\ServerInterface $graphql_server
18+
* The server instance.
19+
*
20+
* @return \Drupal\Core\Access\AccessResultInterface
21+
* The access result.
22+
*/
23+
public function access(AccountInterface $account, ServerInterface $graphql_server) {
24+
if ($account->hasPermission('bypass graphql access')) {
25+
return AccessResult::allowed();
26+
}
27+
28+
$id = $graphql_server->id();
29+
return AccessResult::allowedIfHasPermissions($account, [
30+
"use $id graphql voyager",
31+
"execute $id arbitrary graphql requests",
32+
]);
33+
}
34+
35+
}

src/Controller/RequestController.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public function __construct(array $parameters) {
4141
/**
4242
* Handles graphql requests.
4343
*
44-
* @param \Drupal\graphql\Entity\ServerInterface $server
45-
* The name of the server.
44+
* @param \Drupal\graphql\Entity\ServerInterface $graphql_server
45+
* The server instance.
4646
* @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $operations
4747
* The graphql operation(s) to execute.
4848
*
@@ -51,13 +51,13 @@ public function __construct(array $parameters) {
5151
*
5252
* @throws \Exception
5353
*/
54-
public function handleRequest(ServerInterface $server, $operations) {
54+
public function handleRequest(ServerInterface $graphql_server, $operations) {
5555
if (is_array($operations)) {
56-
return $this->handleBatch($server, $operations);
56+
return $this->handleBatch($graphql_server, $operations);
5757
}
5858

5959
/** @var \GraphQL\Server\OperationParams $operations */
60-
return $this->handleSingle($server, $operations);
60+
return $this->handleSingle($graphql_server, $operations);
6161
}
6262

6363
/**

src/PermissionProvider.php

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Drupal\graphql;
4+
5+
use Drupal\Core\Entity\EntityTypeManagerInterface;
6+
use Drupal\Core\StringTranslation\StringTranslationTrait;
7+
8+
class PermissionProvider {
9+
use StringTranslationTrait;
10+
11+
/**
12+
* The entity type manager service
13+
*
14+
* @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
15+
*/
16+
protected $entityTypeManager;
17+
18+
/**
19+
* PermissionProvider constructor.
20+
*
21+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
22+
*/
23+
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
24+
$this->entityTypeManager = $entityTypeManager;
25+
}
26+
27+
/**
28+
* Collects permissions for the server endpoints.
29+
*/
30+
public function permissions() {
31+
$storage = $this->entityTypeManager->getStorage('graphql_server');
32+
/** @var \Drupal\graphql\Entity\ServerInterface[] $servers */
33+
$servers = $storage->loadMultiple();
34+
$permissions = [];
35+
36+
foreach ($servers as $id => $server) {
37+
$params = ['%name' => $server->label()];
38+
39+
$permissions["execute $id arbitrary graphql requests"] = [
40+
'title' => $this->t('%name: Execute arbitrary requests', $params),
41+
'description' => $this->t('Allows users to execute arbitrary requests on the %name endpoint.', $params),
42+
];
43+
44+
$permissions["execute $id persisted graphql requests"] = [
45+
'title' => $this->t('%name: Execute persisted requests', $params),
46+
'description' => $this->t('Allows users to execute persisted requests on the %name endpoint.', $params),
47+
];
48+
49+
$permissions["use $id graphql explorer"] = [
50+
'title' => $this->t('%name: Use explorer', $params),
51+
'description' => $this->t('Allows users use the explorer interface.', $params),
52+
];
53+
54+
$permissions["execute $id graphql voyager"] = [
55+
'title' => $this->t('%name: Use voyager', $params),
56+
'description' => $this->t('Allows users to use the voyager interface.', $params),
57+
];
58+
}
59+
60+
return $permissions;
61+
}
62+
63+
}

src/Routing/QueryRouteProvider.php src/RouteProvider.php

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?php
22

3-
namespace Drupal\graphql\Routing;
3+
namespace Drupal\graphql;
44

55
use Drupal\Core\Authentication\AuthenticationCollectorInterface;
6-
use Drupal\graphql\Entity\Server;
6+
use Drupal\Core\Entity\EntityTypeManagerInterface;
77
use Symfony\Component\Routing\Route;
88

9-
class QueryRouteProvider {
9+
class RouteProvider {
1010

1111
/**
1212
* The authentication collector service.
@@ -16,21 +16,32 @@ class QueryRouteProvider {
1616
protected $authenticationCollector;
1717

1818
/**
19-
* QueryRouteProvider constructor.
19+
* The entity type manager service.
2020
*
21+
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
22+
*/
23+
protected $entityTypeManager;
24+
25+
/**
26+
* RouteProvider constructor.
27+
*
28+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
29+
* The entity type manager service.
2130
* @param \Drupal\Core\Authentication\AuthenticationCollectorInterface $authenticationCollector
2231
* The authentication collector service.
2332
*/
24-
public function __construct(AuthenticationCollectorInterface $authenticationCollector) {
33+
public function __construct(EntityTypeManagerInterface $entityTypeManager, AuthenticationCollectorInterface $authenticationCollector) {
2534
$this->authenticationCollector = $authenticationCollector;
35+
$this->entityTypeManager = $entityTypeManager;
2636
}
2737

2838
/**
2939
* Collects routes for the server endpoints.
3040
*/
3141
public function routes() {
42+
$storage = $this->entityTypeManager->getStorage('graphql_server');
3243
/** @var \Drupal\graphql\Entity\ServerInterface[] $servers */
33-
$servers = Server::loadMultiple();
44+
$servers = $storage->loadMultiple();
3445
$routes = [];
3546

3647
// Allow all authentication providers by default.
@@ -41,20 +52,20 @@ public function routes() {
4152

4253
$routes["graphql.query.$id"] = (new Route($path))
4354
->addDefaults([
44-
'server' => $id,
55+
'graphql_server' => $id,
4556
'_graphql' => TRUE,
4657
'_controller' => '\Drupal\graphql\Controller\RequestController::handleRequest',
4758
'_disable_route_normalizer' => TRUE,
4859
])
4960
->addRequirements([
50-
'_graphql_query_access' => 'TRUE',
61+
'_graphql_query_access' => 'graphql_server:{graphql_server}',
5162
'_format' => 'json',
5263
])
5364
->addOptions([
5465
'_auth' => $auth,
5566
'no_cache' => TRUE,
5667
'default_url_options' => ['path_processing' => FALSE],
57-
'parameters' => ['server' => ['type' => 'entity:graphql_server']]
68+
'parameters' => ['graphql_server' => ['type' => 'entity:graphql_server']]
5869
]);
5970
}
6071

tests/src/Kernel/Framework/PermissionsTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function testNoPermissions() {
5757
* The user is allowed to post any queries.
5858
*/
5959
public function testFullQueryAccess() {
60-
$this->setUpCurrentUser([], ['execute graphql requests']);
60+
$this->setUpCurrentUser([], ["execute {$this->server->id()} arbitrary graphql requests"]);
6161

6262
// All queries should work.
6363
$this->assertEquals(200, $this->query('{ root }')->getStatusCode());

tests/src/Kernel/GraphQLTestBase.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ protected function defaultCacheContexts() {
120120
* @return array
121121
*/
122122
protected function userPermissions() {
123-
return ['access content', 'execute graphql requests'];
123+
return ['access content', 'bypass graphql access'];
124124
}
125125

126126
}

0 commit comments

Comments
 (0)