diff --git a/modules/custom/dkan_common/dkan_common.services.yml b/modules/custom/dkan_common/dkan_common.services.yml
index 7a892a8c5..8e01f5602 100644
--- a/modules/custom/dkan_common/dkan_common.services.yml
+++ b/modules/custom/dkan_common/dkan_common.services.yml
@@ -1,5 +1,8 @@
services:
- dkan.factory:
- class: Drupal\dkan_common\Service\Factory
- dkan.json_util:
- class: Drupal\dkan_common\Service\JsonUtil
+ dkan.factory:
+ class: Drupal\dkan_common\Service\Factory
+ dkan.json_util:
+ class: Drupal\dkan_common\Service\JsonUtil
+ plugin.manager.dkan_common.data_modifier:
+ class: \Drupal\dkan_common\Plugin\DataModifierManager
+ parent: default_plugin_manager
diff --git a/modules/custom/dkan_common/src/Annotation/DataModifier.php b/modules/custom/dkan_common/src/Annotation/DataModifier.php
new file mode 100644
index 000000000..43f0e8eaf
--- /dev/null
+++ b/modules/custom/dkan_common/src/Annotation/DataModifier.php
@@ -0,0 +1,48 @@
+pluginManager->getDefinitions() as $definition) {
+ $plugins[] = $this->pluginManager->createInstance($definition['id']);
+ }
+ return $plugins;
+ }
+
+}
diff --git a/modules/custom/dkan_common/src/Plugin/DataModifierBase.php b/modules/custom/dkan_common/src/Plugin/DataModifierBase.php
new file mode 100644
index 000000000..0dc4d45ad
--- /dev/null
+++ b/modules/custom/dkan_common/src/Plugin/DataModifierBase.php
@@ -0,0 +1,32 @@
+getPluginDefinition()['result']->render();
+ }
+
+ /**
+ * Return the http code annotation.
+ *
+ * @return int
+ * The http code.
+ */
+ public function httpCode() : int {
+ return (int) $this->getPluginDefinition()['code'];
+ }
+
+}
diff --git a/modules/custom/dkan_common/src/Plugin/DataModifierInterface.php b/modules/custom/dkan_common/src/Plugin/DataModifierInterface.php
new file mode 100644
index 000000000..c549c42fa
--- /dev/null
+++ b/modules/custom/dkan_common/src/Plugin/DataModifierInterface.php
@@ -0,0 +1,60 @@
+alterInfo('dkan_common_data_modifier_info');
+ $this->setCacheBackend($cache_backend, 'dkan_common_data_modifier_plugins');
+ }
+
+}
diff --git a/modules/custom/dkan_common/tests/src/Unit/Plugin/DataModifierManagerTest.php b/modules/custom/dkan_common/tests/src/Unit/Plugin/DataModifierManagerTest.php
new file mode 100644
index 000000000..23ca73e9f
--- /dev/null
+++ b/modules/custom/dkan_common/tests/src/Unit/Plugin/DataModifierManagerTest.php
@@ -0,0 +1,29 @@
+assertTrue(is_object($manager));
+ }
+
+}
diff --git a/modules/custom/dkan_metastore/dkan_metastore.services.yml b/modules/custom/dkan_metastore/dkan_metastore.services.yml
index 384ae156d..9c4a0269a 100644
--- a/modules/custom/dkan_metastore/dkan_metastore.services.yml
+++ b/modules/custom/dkan_metastore/dkan_metastore.services.yml
@@ -4,6 +4,8 @@ services:
arguments:
- '@dkan_schema.schema_retriever'
- '@dkan_metastore.sae_factory'
+ calls:
+ - [setDataModifierPlugins, ['@plugin.manager.dkan_common.data_modifier']]
dkan_metastore.sae_factory:
class: \Drupal\dkan_metastore\Factory\Sae
arguments:
diff --git a/modules/custom/dkan_metastore/src/Service.php b/modules/custom/dkan_metastore/src/Service.php
index b61fbe150..962ffa9ac 100644
--- a/modules/custom/dkan_metastore/src/Service.php
+++ b/modules/custom/dkan_metastore/src/Service.php
@@ -2,6 +2,8 @@
namespace Drupal\dkan_metastore;
+use Drupal\dkan_common\DataModifierPluginTrait;
+use Drupal\dkan_common\Plugin\DataModifierManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\dkan_metastore\Factory\Sae;
@@ -14,6 +16,7 @@
* Service.
*/
class Service implements ContainerInjectionInterface {
+ use DataModifierPluginTrait;
/**
* SAE Factory.
@@ -49,6 +52,17 @@ public function __construct(SchemaRetriever $schemaRetriever, Sae $saeFactory) {
$this->saeFactory = $saeFactory;
}
+ /**
+ * Setter to discover data modifier plugins.
+ *
+ * @param \Drupal\dkan_common\Plugin\DataModifierManager $pluginManager
+ * Injected plugin manager.
+ */
+ public function setDataModifierPlugins(DataModifierManager $pluginManager) {
+ $this->pluginManager = $pluginManager;
+ $this->plugins = $this->discover();
+ }
+
/**
* Get schemas.
*/
@@ -86,7 +100,10 @@ public function getAll($schema_id): array {
// $datasets is an array of JSON encoded string. Needs to be unflattened.
$unflattened = array_map(
- function ($json_string) {
+ function ($json_string) use ($schema_id) {
+ if (!empty($this->plugins)) {
+ $json_string = $this->modifyData($schema_id, $json_string);
+ }
return json_decode($json_string);
},
$datasets
@@ -107,8 +124,35 @@ function ($json_string) {
* The json data.
*/
public function get($schema_id, $identifier): string {
- return $this->getEngine($schema_id)
+ $data = $this->getEngine($schema_id)
->get($identifier);
+ if (!empty($this->plugins)) {
+ $data = $this->modifyData($schema_id, $data);
+ }
+ return $data;
+ }
+
+ /**
+ * Provides data modifiers plugins an opportunity to act.
+ *
+ * @param string $schema_id
+ * The {schema_id} slug from the HTTP request.
+ * @param string $data
+ * The Json input.
+ *
+ * @return string
+ * The Json, modified by each applicable discovered data modifier plugins.
+ */
+ private function modifyData(string $schema_id, string $data) {
+ $dataObj = json_decode($data);
+
+ foreach ($this->plugins as $plugin) {
+ if ($plugin->requiresModification($schema_id, $dataObj)) {
+ $dataObj = $plugin->modify($schema_id, $dataObj);
+ }
+ }
+
+ return json_encode($dataObj);
}
/**
diff --git a/modules/custom/dkan_metastore/src/WebServiceApiDocs.php b/modules/custom/dkan_metastore/src/WebServiceApiDocs.php
index 7e4eaa0a9..b163c2c7d 100644
--- a/modules/custom/dkan_metastore/src/WebServiceApiDocs.php
+++ b/modules/custom/dkan_metastore/src/WebServiceApiDocs.php
@@ -2,6 +2,8 @@
namespace Drupal\dkan_metastore;
+use Drupal\dkan_common\DataModifierPluginTrait;
+use Drupal\dkan_common\Plugin\DataModifierManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\dkan_common\JsonResponseTrait;
@@ -13,6 +15,7 @@
*/
class WebServiceApiDocs implements ContainerInjectionInterface {
use JsonResponseTrait;
+ use DataModifierPluginTrait;
/**
* List of endpoints to keep for dataset-specific docs.
@@ -47,7 +50,8 @@ class WebServiceApiDocs implements ContainerInjectionInterface {
public static function create(ContainerInterface $container) {
return new WebServiceApiDocs(
$container->get("dkan_api.docs"),
- $container->get("dkan_metastore.service")
+ $container->get("dkan_metastore.service"),
+ $container->get('plugin.manager.dkan_common.data_modifier')
);
}
@@ -58,10 +62,15 @@ public static function create(ContainerInterface $container) {
* Serves openapi spec for dataset-related endpoints.
* @param \Drupal\dkan_metastore\Service $metastoreService
* Metastore service.
+ * @param \Drupal\dkan_common\Plugin\DataModifierManager $pluginManager
+ * Metastore plugin manager.
*/
- public function __construct(Docs $docsController, Service $metastoreService) {
+ public function __construct(Docs $docsController, Service $metastoreService, DataModifierManager $pluginManager) {
$this->docsController = $docsController;
$this->metastoreService = $metastoreService;
+ $this->pluginManager = $pluginManager;
+
+ $this->plugins = $this->discover();
}
/**
@@ -109,6 +118,26 @@ private function modifyDatasetEndpoint(array $spec, string $identifier) {
return $spec;
}
+ /**
+ * Provides data modifiers plugins an opportunity to act.
+ *
+ * @param string $identifier
+ * The distribution's identifier.
+ *
+ * @return bool
+ * TRUE if sql endpoint docs needs to be protected, FALSE otherwise.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\PluginException
+ */
+ private function modifyData(string $identifier) {
+ foreach ($this->plugins as $plugin) {
+ if ($plugin->requiresModification('distribution', $identifier)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
/**
* Modify the generic sql endpoint to be specific to the current dataset.
*
@@ -121,7 +150,10 @@ private function modifyDatasetEndpoint(array $spec, string $identifier) {
* Spec with dataset-specific datastore sql endpoint.
*/
private function modifySqlEndpoint(array $spec, string $identifier) {
- if (isset($spec['paths']['/api/1/datastore/sql'])) {
+ if ($this->modifyData($identifier)) {
+ unset($spec['paths']['/api/1/datastore/sql']);
+ }
+ elseif (isset($spec['paths']['/api/1/datastore/sql'])) {
unset($spec['paths']['/api/1/datastore/sql']['get']['parameters']);
$spec = $this->replaceDistributions($spec, $identifier);
$spec['tags'][] = ["name" => "SQL Query"];
@@ -196,15 +228,37 @@ private function replaceDistributions(array $spec, string $identifier) {
// Create and customize a path for each dataset distribution/resource.
if (isset($data->distribution)) {
foreach ($data->distribution as $dist) {
- $path = "/api/1/datastore/sql?query=[SELECT * FROM {$dist->identifier}];";
-
- $spec['paths'][$path] = $spec['paths']['/api/1/datastore/sql'];
- $spec['paths'][$path]['get']['summary'] = $dist->data->title ?? "";
- $spec['paths'][$path]['get']['description'] = $dist->data->description ?? "";
+ $spec = $this->replaceDistribution($dist, $spec, $identifier);
}
unset($spec['paths']['/api/1/datastore/sql']);
}
return $spec;
}
+ /**
+ * Replace a single distribution within the spec.
+ *
+ * @param mixed $dist
+ * A distribution object.
+ * @param array $spec
+ * The original spec array.
+ * @param string $identifier
+ * The dataset uuid.
+ *
+ * @return array
+ * Modified spec.
+ */
+ private function replaceDistribution($dist, array $spec, string $identifier) {
+ $path = "/api/1/datastore/sql?query=[SELECT * FROM {$dist->identifier}];";
+
+ $spec['paths'][$path] = $spec['paths']['/api/1/datastore/sql'];
+ if (isset($dist->data->title)) {
+ $spec['paths'][$path]['get']['summary'] = $dist->data->title;
+ }
+ if (isset($dist->data->description)) {
+ $spec['paths'][$path]['get']['description'] = $dist->data->description;
+ }
+ return $spec;
+ }
+
}
diff --git a/modules/custom/dkan_metastore/tests/src/Unit/WebServiceApiDocsTest.php b/modules/custom/dkan_metastore/tests/src/Unit/WebServiceApiDocsTest.php
index 27f1c7888..3c23b73dd 100644
--- a/modules/custom/dkan_metastore/tests/src/Unit/WebServiceApiDocsTest.php
+++ b/modules/custom/dkan_metastore/tests/src/Unit/WebServiceApiDocsTest.php
@@ -2,6 +2,9 @@
namespace Drupal\Tests\dkan_metastore\Unit;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\dkan_common\Plugin\DataModifierManager;
+use Drupal\dkan_common\Plugin\DataModifierBase;
use PHPUnit\Framework\TestCase;
use Drupal\Core\Serialization\Yaml;
use Drupal\dkan_api\Controller\Docs;
@@ -17,15 +20,48 @@
class WebServiceApiDocsTest extends TestCase {
/**
- *
+ * Tests dataset-specific docs without data modifier plugin.
*/
- public function testGetDatasetSpecific() {
- $mockChain = $this->getCommonMockChain();
+ public function testDatasetSpecificDocsWithoutSqlModifier() {
+ $dataset = json_encode([
+ 'distribution' => [
+ [
+ 'identifier' => 'dist-1234',
+ 'data' => [
+ 'title' => 'Title',
+ 'description' => 'Description',
+ ],
+ ],
+ ],
+ ]);
+
+ $mockChain = $this->getCommonMockChain()
+ ->add(Service::class, "get", $dataset)
+ ->add(DataModifierManager::class, 'getDefinitions', [])
+ ->add(SelectInterface::class, 'fetchCol', []);
$controller = WebServiceApiDocs::create($mockChain->getMock());
$response = $controller->getDatasetSpecific(1);
- $spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"tags":[{"name":"Dataset"},{"name":"SQL Query"}],"paths":{"\/api\/1\/datastore\/sql":{"get":{"summary":"Query resources","tags":["SQL Query"],"responses":{"200":{"description":"Ok"}}}},"\/api\/1\/metastore\/schemas\/dataset\/items\/1":{"get":{"summary":"Get this dataset","tags":["Dataset"],"responses":{"200":{"description":"Ok"}}}}}}';
+ $spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"tags":[{"name":"Dataset"},{"name":"SQL Query"}],"paths":{"\/api\/1\/metastore\/schemas\/dataset\/items\/1":{"get":{"summary":"Get this dataset","tags":["Dataset"],"responses":{"200":{"description":"Ok"}}}},"\/api\/1\/datastore\/sql?query=[SELECT * FROM dist-1234];":{"get":{"summary":"Title","tags":["SQL Query"],"responses":{"200":{"description":"Ok"}},"description":"Description"}}}}';
+
+ $this->assertEquals($spec, $response->getContent());
+ }
+
+ /**
+ * Tests dataset-specific docs when SQL endpoint is protected.
+ */
+ public function testDatasetSpecificDocsWithSqlModifier() {
+ $mockChain = $this->getCommonMockChain()
+ ->add(Service::class, "get", "{}")
+ ->add(DataModifierManager::class, 'getDefinitions', [['id' => 'foobar']])
+ ->add(DataModifierManager::class, 'createInstance', DataModifierBase::class)
+ ->add(DataModifierBase::class, 'requiresModification', TRUE);
+
+ $controller = WebServiceApiDocs::create($mockChain->getMock());
+ $response = $controller->getDatasetSpecific(1);
+
+ $spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"tags":[{"name":"Dataset"}],"paths":{"\/api\/1\/metastore\/schemas\/dataset\/items\/1":{"get":{"summary":"Get this dataset","tags":["Dataset"],"responses":{"200":{"description":"Ok"}}}}}}';
$this->assertEquals($spec, $response->getContent());
}
@@ -42,9 +78,10 @@ private function getCommonMockChain() {
$mockChain->add(ContainerInterface::class, 'get',
(new Options)->add('dkan_api.docs', Docs::class)
->add('dkan_metastore.service', Service::class)
+ ->add('plugin.manager.dkan_common.data_modifier', DataModifierManager::class)
)
- ->add(Docs::class, "getJsonFromYmlFile", $serializer->decode($yamlSpec))
- ->add(Service::class, "get", "{}");
+ ->add(Docs::class, "getJsonFromYmlFile", $serializer->decode($yamlSpec));
+
return $mockChain;
}
diff --git a/modules/custom/dkan_non_public/dkan_non_public.info.yml b/modules/custom/dkan_non_public/dkan_non_public.info.yml
new file mode 100644
index 000000000..75e499a87
--- /dev/null
+++ b/modules/custom/dkan_non_public/dkan_non_public.info.yml
@@ -0,0 +1,7 @@
+name: 'Non Public Data Modifier'
+description: 'Protects the resources of datasets with non-public access level, on publicly accessible endpoints.'
+type: module
+core: 8.x
+package: DKAN
+dependencies:
+ - dkan_alt_api
diff --git a/modules/custom/dkan_non_public/src/Plugin/DataModifier/NonPublicResourceProtector.php b/modules/custom/dkan_non_public/src/Plugin/DataModifier/NonPublicResourceProtector.php
new file mode 100644
index 000000000..83f69a91d
--- /dev/null
+++ b/modules/custom/dkan_non_public/src/Plugin/DataModifier/NonPublicResourceProtector.php
@@ -0,0 +1,298 @@
+configuration = $configuration;
+ $this->pluginId = $plugin_id;
+ $this->pluginDefinition = $plugin_definition;
+ $this->database = $database;
+ $this->routeMatch = $routeMatch;
+ $this->uuid5 = $uuid5;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('database'),
+ $container->get('current_route_match'),
+ $container->get('dkan_data.uuid5')
+ );
+ }
+
+ /**
+ * Check if a resource needs to be protected.
+ *
+ * @param string $schema
+ * The schema id.
+ * @param object|string $data
+ * Object, json or identifier string representing a dataset or distribution.
+ *
+ * @return bool
+ * TRUE if the data requires modification, FALSE otherwise.
+ */
+ public function requiresModification(string $schema, $data) {
+ return in_array($schema, $this->schemasToModify)
+ && !$this->alternateEndpoint()
+ && $this->accessLevel($schema, $data) === 'non-public';
+ }
+
+ /**
+ * Check if user requests one of the alternate dkan_alt_api endpoint.
+ *
+ * @return bool
+ * TRUE from alternate endpoints, FALSE otherwise.
+ */
+ private function alternateEndpoint() {
+ $routeName = $this->routeMatch->getRouteName();
+ return strpos($routeName, 'dkan_alt_api.') === 0;
+ }
+
+ /**
+ * Check if a dataset or a distribution's parent has non-public access level.
+ *
+ * @param string $schema
+ * The schema id.
+ * @param mixed $data
+ * Object, json or identifier string representing a dataset or distribution.
+ *
+ * @return bool
+ * TRUE if non-public, FALSE otherwise.
+ */
+ private function accessLevel(string $schema, $data) : string {
+ // For distributions, check their parent dataset's access level.
+ if ('distribution' === $schema) {
+ return $this->parentDatasetAccessLevel($data);
+ }
+ return $this->datasetAccessLevel($data);
+ }
+
+ /**
+ * Returns a dataset's access level.
+ *
+ * @param object|string $data
+ * A dataset object or json string.
+ *
+ * @return string
+ * The access level of the dataset.
+ */
+ private function datasetAccessLevel($data) {
+ if (is_string($data)) {
+ $data = json_decode($data);
+ }
+ return $data->accessLevel;
+ }
+
+ /**
+ * Get the access level of a distribution's parent dataset.
+ *
+ * @param object|string $dist
+ * Object, json or identifier string representing a distribution.
+ *
+ * @return string
+ * The parent dataset's access level.
+ */
+ private function parentDatasetAccessLevel($dist) {
+ $identifier = $this->getIdentifier($dist);
+ $parentDataset = $this->getParentDataset($identifier);
+ return $this->datasetAccessLevel($parentDataset);
+ }
+
+ /**
+ * Get a distribution's identifier, from its object or json string.
+ *
+ * @param object|string $dist
+ * Object, json or identifier string representing a distribution.
+ *
+ * @return string
+ * The distribution's identifier.
+ */
+ private function getIdentifier($dist) : string {
+ if (is_string($dist)) {
+ if ($this->uuid5->isValid($dist)) {
+ return $dist;
+ }
+ $dist = json_decode($dist);
+ }
+ return $dist->identifier;
+ }
+
+ /**
+ * Get the metadata of a distribution's parent dataset.
+ *
+ * @param string $identifier
+ * The distribution's identifier.
+ *
+ * @return string|bool
+ * The dataset's metadata, or FALSE.
+ */
+ private function getParentDataset($identifier) {
+ $datasets = $this->database->select('node__field_json_metadata', 'm')
+ ->condition('m.field_json_metadata_value', '%accessLevel%', 'LIKE')
+ ->condition('m.field_json_metadata_value', "%{$identifier}%", 'LIKE')
+ ->fields('m', ['field_json_metadata_value'])
+ ->execute()
+ ->fetchCol();
+
+ return reset($datasets);
+ }
+
+ /**
+ * Protect potentially sensitive data in a dataset or distribution.
+ *
+ * @param string $schema
+ * The schema id.
+ * @param object|string $data
+ * Object, json or identifier string representing a dataset or distribution.
+ *
+ * @return mixed
+ * Modified data, or FALSE.
+ */
+ public function modify(string $schema, $data) {
+ if ('distribution' === $schema) {
+ return $this->protectDistribution($data);
+ }
+ return $this->protectDataset($data);
+ }
+
+ /**
+ * Protect dataset object or string.
+ *
+ * @param object|string $dataset
+ * A dataset object or json string.
+ *
+ * @return object|string
+ * The protected dataset.
+ */
+ private function protectDataset($dataset) {
+ if (is_string($dataset)) {
+ $datasetObj = json_decode($dataset);
+ $datasetObj = $this->protectDatasetObject($datasetObj);
+ return json_encode($datasetObj);
+ }
+ return $this->protectDatasetObject($dataset);
+ }
+
+ /**
+ * Protect dataset object.
+ *
+ * @param object $dataset
+ * The dataset object.
+ *
+ * @return object
+ * The protected dataset object
+ */
+ private function protectDatasetObject($dataset) {
+ if (isset($dataset->distribution) && is_array($dataset->distribution)) {
+ foreach ($dataset->distribution as $key => &$dist) {
+ $dataset->distribution[$key] = $this->protectDistributionObject($dist);
+ }
+ }
+ return $dataset;
+ }
+
+ /**
+ * Protect distribution, both object and json strings.
+ *
+ * @param object|string $dist
+ * A distribution object or json string.
+ *
+ * @return false|mixed|string
+ * A protected distribution.
+ */
+ private function protectDistribution($dist) {
+ if (is_string($dist)) {
+ $distObj = json_decode($dist);
+ $distObj = $this->protectDistributionObject($distObj);
+ return json_encode($distObj);
+ }
+ return $this->protectDistributionObject($dist);
+ }
+
+ /**
+ * Protect distribution object.
+ *
+ * @param object $dist
+ * A distribution object.
+ *
+ * @return object
+ * A protected distribution object, with an explanation.
+ */
+ private function protectDistributionObject($dist) {
+ unset($dist);
+ return (object) ['title' => $this->message()];
+ }
+
+}
diff --git a/modules/custom/dkan_non_public/tests/src/Unit/Plugin/DataModifier/NonPublicResourceProtectorTest.php b/modules/custom/dkan_non_public/tests/src/Unit/Plugin/DataModifier/NonPublicResourceProtectorTest.php
new file mode 100644
index 000000000..181f581a6
--- /dev/null
+++ b/modules/custom/dkan_non_public/tests/src/Unit/Plugin/DataModifier/NonPublicResourceProtectorTest.php
@@ -0,0 +1,117 @@
+ [
+ 'dataset',
+ (object) ['accessLevel' => 'public'],
+ [],
+ FALSE,
+ ],
+ 'resource object with non-public parent dataset' => [
+ 'distribution',
+ (object) ['identifier' => '1', 'data' => ['foo' => 'bar']],
+ ['{"accessLevel":"non-public"}'],
+ TRUE,
+ ],
+ 'resource json string with public parent dataset' => [
+ 'distribution',
+ '{"identifier":"1","data":{"foo":"bar"}}',
+ ['{"accessLevel":"public"}'],
+ FALSE,
+ ],
+ 'resource uuid with public parent dataset' => [
+ 'distribution',
+ '6b1f0bb4-60cd-5b27-8e03-5fecff4c7e2a',
+ ['{"accessLevel":"non-public"}'],
+ TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider requiresModificationProvider
+ */
+ public function testRequiresModification(string $schema, $data, $datasets, $expected) {
+ $container = $this->getCommonMockChain()
+ ->add(StatementInterface::class, 'fetchCol', $datasets);
+
+ $plugin = NonPublicResourceProtector::create(
+ $container->getMock(),
+ [],
+ 'non_public_resource_protector',
+ []
+ );
+
+ $this->assertEquals($expected, $plugin->requiresModification($schema, $data));
+ }
+
+ public function modifyProvider() {
+ return [
+ 'dataset json string without resources' => [
+ 'dataset',
+ '{"foo":"bar"}',
+ '{"foo":"bar"}',
+ ],
+ 'dataset object with empty distribution array' => [
+ 'dataset',
+ (object) ["foo" => "bar", "distribution" => []],
+ (object) ["foo" => "bar", "distribution" => []],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider modifyProvider
+ */
+ public function testModify($schema, $data, $expected) {
+ $plugin = NonPublicResourceProtector::create(
+ $this->getCommonMockChain()->getMock(),
+ [],
+ 'non_public_resource_protector',
+ []
+ );
+
+ $this->assertEquals($expected, $plugin->modify($schema, $data));
+ }
+
+ public function getCommonMockChain() {
+ $pluginMessage = "Resource hidden since dataset access level is non-public.";
+
+ $options = (new Options())
+ ->add('database', Connection::class)
+ ->add('current_route_match', RouteMatchInterface::class)
+ ->add('dkan_data.uuid5', Uuid5::class);
+
+ return (new Chain($this))
+ ->add(Container::class, 'get', $options)
+ ->add(Connection::class, 'select', SelectInterface::class)
+ ->add(SelectInterface::class, 'condition', ConditionInterface::class)
+ ->add(ConditionInterface::class, 'condition', ConditionInterface::class)
+ ->add(ConditionInterface::class, 'fields', SelectInterface::class)
+ ->add(SelectInterface::class, 'execute', StatementInterface::class)
+ ->add(StatementInterface::class, 'fetchCol', [])
+ ->add(DataModifierBase::class, 'message', $pluginMessage);
+ }
+
+}
diff --git a/modules/custom/dkan_sql_endpoint/src/Controller/Api.php b/modules/custom/dkan_sql_endpoint/src/Controller/Api.php
index f5905de00..20c6f0e4f 100644
--- a/modules/custom/dkan_sql_endpoint/src/Controller/Api.php
+++ b/modules/custom/dkan_sql_endpoint/src/Controller/Api.php
@@ -2,6 +2,8 @@
namespace Drupal\dkan_sql_endpoint\Controller;
+use Drupal\dkan_common\DataModifierPluginTrait;
+use Drupal\dkan_common\Plugin\DataModifierManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Database\Connection;
@@ -17,6 +19,7 @@
*/
class Api implements ContainerInjectionInterface {
use JsonResponseTrait;
+ use DataModifierPluginTrait;
private $service;
private $database;
@@ -37,7 +40,8 @@ public static function create(ContainerInterface $container) {
$container->get('database'),
$container->get('dkan_datastore.service.factory.resource'),
$container->get('request_stack'),
- $container->get('dkan_datastore.database_table_factory')
+ $container->get('dkan_datastore.database_table_factory'),
+ $container->get('plugin.manager.dkan_common.data_modifier')
);
}
@@ -49,13 +53,17 @@ public function __construct(
Connection $database,
Resource $resourceServiceFactory,
RequestStack $requestStack,
- DatabaseTableFactory $databaseTableFactory
+ DatabaseTableFactory $databaseTableFactory,
+ DataModifierManager $pluginManager
) {
$this->service = $service;
$this->database = $database;
$this->resourceServiceFactory = $resourceServiceFactory;
$this->requestStack = $requestStack;
$this->databaseTableFactory = $databaseTableFactory;
+ $this->pluginManager = $pluginManager;
+
+ $this->plugins = $this->discover();
}
/**
@@ -103,7 +111,11 @@ private function runQuery(string $query) {
return $this->getResponseFromException($e);
}
- $databaseTable = $this->getDatabaseTable($this->service->getTableName($query));
+ $uuid = $this->service->getTableName($query);
+ if ($modifyResponse = $this->modifyData($uuid)) {
+ return $modifyResponse;
+ }
+ $databaseTable = $this->getDatabaseTable($uuid);
try {
$result = $databaseTable->query($queryObject);
@@ -115,6 +127,24 @@ private function runQuery(string $query) {
return $this->getResponse($result, 200);
}
+ /**
+ * Provides data modifiers plugins an opportunity to act.
+ *
+ * @param string $identifier
+ * The distribution's identifier.
+ *
+ * @return object|bool
+ * The json response if sql endpoint docs needs modifying, FALSE otherwise.
+ */
+ private function modifyData(string $identifier) {
+ foreach ($this->plugins as $plugin) {
+ if ($plugin->requiresModification('distribution', $identifier)) {
+ return $this->getResponse((object) ["message" => $plugin->message()], $plugin->httpCode());
+ }
+ }
+ return FALSE;
+ }
+
/**
* Private.
*/
diff --git a/modules/custom/dkan_sql_endpoint/tests/src/Unit/Controller/ApiTest.php b/modules/custom/dkan_sql_endpoint/tests/src/Unit/Controller/ApiTest.php
index 9fa970881..0e4d3b0e0 100644
--- a/modules/custom/dkan_sql_endpoint/tests/src/Unit/Controller/ApiTest.php
+++ b/modules/custom/dkan_sql_endpoint/tests/src/Unit/Controller/ApiTest.php
@@ -8,6 +8,8 @@
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\Container;
+use Drupal\dkan_common\Plugin\DataModifierBase;
+use Drupal\dkan_common\Plugin\DataModifierManager;
use MockChain\Chain;
use MockChain\Options;
use Drupal\dkan_datastore\Storage\DatabaseTable;
@@ -30,7 +32,7 @@ class ApiTest extends TestCase {
*
*/
public function test() {
- $controller = Api::create($this->getContainer());
+ $controller = Api::create($this->getCommonMockChain()->getMock());
$response = $controller->runQueryGet();
$this->assertEquals("[]", $response->getContent());
}
@@ -39,7 +41,7 @@ public function test() {
*
*/
public function test2() {
- $controller = Api::create($this->getContainer());
+ $controller = Api::create($this->getCommonMockChain()->getMock());
$response = $controller->runQueryPost();
$this->assertEquals("[]", $response->getContent());
}
@@ -47,7 +49,27 @@ public function test2() {
/**
*
*/
- private function getContainer() {
+ public function testWithDataModifierPlugin() {
+ $pluginMessage = "Resource hidden since dataset access level is non-public.";
+ $pluginCode = 401;
+
+ $container = $this->getCommonMockChain()
+ ->add(DataModifierManager::class, 'getDefinitions', [['id' => 'foobar']])
+ ->add(DataModifierManager::class, 'createInstance', DataModifierBase::class)
+ ->add(DataModifierBase::class, 'requiresModification', TRUE)
+ ->add(DataModifierBase::class, 'message', $pluginMessage)
+ ->add(DataModifierBase::class, 'httpCode', $pluginCode);
+
+ $controller = Api::create($container->getMock());
+ $response = $controller->runQueryGet();
+ $expected = '{"message":"Resource hidden since dataset access level is non-public."}';
+ $this->assertEquals($expected, $response->getContent());
+ }
+
+ /**
+ * @return \MockChain\Chain
+ */
+ public function getCommonMockChain() {
$container = (new Chain($this))
->add(Container::class, "get", ConfigFactory::class)
->add(ConfigFactory::class, "get", ImmutableConfig::class)
@@ -59,12 +81,13 @@ private function getContainer() {
->add("database", Connection::class)
->add('dkan_datastore.service.factory.resource', ResourceServiceFactory::class)
->add('request_stack', RequestStack::class)
- ->add('dkan_datastore.database_table_factory', DatabaseTableFactory::class);
+ ->add('dkan_datastore.database_table_factory', DatabaseTableFactory::class)
+ ->add('plugin.manager.dkan_common.data_modifier', DataModifierManager::class);
$query = '[SELECT * FROM abc][WHERE abc = \'blah\'][ORDER BY abc DESC][LIMIT 1 OFFSET 3];';
$body = json_encode(["query" => $query]);
- $container = (new Chain($this))
+ return (new Chain($this))
->add(Container::class, "get", $options)
->add(RequestStack::class, 'getCurrentRequest', Request::class)
->add(Request::class, 'get', $query)
@@ -76,9 +99,9 @@ private function getContainer() {
->add(Resource::class, 'getId', "1")
->add(DatabaseTableFactory::class, 'getInstance', DatabaseTable::class)
->add(DatabaseTable::class, 'query', [])
- ->getMock();
-
- return $container;
+ ->add(DataModifierManager::class, 'getDefinitions', [])
+ ->add(DataModifierManager::class, 'createInstance', DataModifierBase::class)
+ ->add(DataModifierBase::class, 'requiresModification', FALSE);
}
}
diff --git a/phpunit.xml b/phpunit.xml
index 7064c9f96..48eaf28b4 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -16,6 +16,7 @@
modules/custom/dkan_frontend/tests/src/Unit
modules/custom/dkan_harvest/test/src/Unit
modules/custom/dkan_metastore/tests/src/Unit
+ modules/custom/dkan_non_public/tests/src/Unit
modules/custom/dkan_schema/tests/src/Unit
modules/custom/dkan_sql_endpoint/tests/src/Unit
@@ -44,6 +45,7 @@
modules/custom/dkan_harvest/src
modules/custom/dkan_lunr/src
modules/custom/dkan_metastore/src
+ modules/custom/dkan_non_public/src
modules/custom/dkan_schema/src
modules/custom/dkan_sql_endpoint/src