Commit 5987069e authored by alexpott's avatar alexpott

Issue #2228141 by juampynr, eiriksm, dawehner, almaudoh, lokapujya,...

Issue #2228141 by juampynr, eiriksm, dawehner, almaudoh, lokapujya, mohit_aghera, clemens.tolboom, manojapare, Lendude, Sonal.Sangale, Wim Leers, alexpott, damiankloip, andypost: Add authentication support to REST views
parent 765aa9e7
......@@ -3,6 +3,13 @@
views.display.rest_export:
type: views_display_path
label: 'REST display options'
mapping:
auth:
type: sequence
label: 'Authentication'
sequence:
type: string
label: 'Authentication Provider'
views.row.data_field:
type: views_row
......
......@@ -39,6 +39,30 @@ function rest_update_8201() {
->save();
}
/**
* Re-save all views with a REST display to add new auth defaults.
*/
function rest_update_8202() {
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('views.view.') as $view_config_name) {
$save = FALSE;
$view = $config_factory->getEditable($view_config_name);
$displays = $view->get('display');
foreach ($displays as $display_name => &$display) {
if ($display['display_plugin'] == 'rest_export') {
if (!isset($display['display_options']['auth'])) {
$display['display_options']['auth'] = [];
$save = TRUE;
}
}
}
if ($save) {
$view->set('display', $displays);
$view->save(TRUE);
}
}
}
/**
* @} End of "defgroup updates-8.1.x-to-8.2.x".
*/
......@@ -4,6 +4,7 @@
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteProviderInterface;
......@@ -77,6 +78,20 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*/
protected $renderer;
/**
* The collector of authentication providers.
*
* @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
*/
protected $authenticationCollector;
/**
* The authentication providers, keyed by ID.
*
* @var string[]
*/
protected $authenticationProviders;
/**
* Constructs a RestExport object.
*
......@@ -92,11 +107,14 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
* The state key value store.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param string[] $authentication_providers
* The authentication providers, keyed by ID.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->renderer = $renderer;
$this->authenticationProviders = $authentication_providers;
}
/**
......@@ -109,7 +127,9 @@ public static function create(ContainerInterface $container, array $configuratio
$plugin_definition,
$container->get('router.route_provider'),
$container->get('state'),
$container->get('renderer')
$container->get('renderer'),
$container->getParameter('authentication_providers')
);
}
/**
......@@ -199,12 +219,25 @@ public function getContentType() {
return $this->contentType;
}
/**
* Gets the auth options available.
*
* @return string[]
* An array to use as value for "#options" in the form element.
*/
public function getAuthOptions() {
return array_combine($this->authenticationProviders, $this->authenticationProviders);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
// Options for REST authentication.
$options['auth'] = ['default' => []];
// Set the default style plugin to 'json'.
$options['style']['contains']['type']['default'] = 'serializer';
$options['row']['contains']['type']['default'] = 'data_entity';
......@@ -225,6 +258,9 @@ protected function defineOptions() {
public function optionsSummary(&$categories, &$options) {
parent::optionsSummary($categories, $options);
// Authentication.
$auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
unset($categories['page'], $categories['exposed']);
// Hide some settings, as they aren't useful for pure data output.
unset($options['show_admin_links'], $options['analyze-theme']);
......@@ -239,6 +275,11 @@ public function optionsSummary(&$categories, &$options) {
$options['path']['category'] = 'path';
$options['path']['title'] = $this->t('Path');
$options['auth'] = array(
'category' => 'path',
'title' => $this->t('Authentication'),
'value' => views_ui_truncate($auth, 24),
);
// Remove css/exposed form settings, as they are not used for the data
// display.
......@@ -247,6 +288,34 @@ public function optionsSummary(&$categories, &$options) {
unset($options['css_class']);
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
if ($form_state->get('section') === 'auth') {
$form['#title'] .= $this->t('The supported authentication methods for this view');
$form['auth'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Authentication methods'),
'#description' => $this->t('These are the supported authentication providers for this view. When this view is requested, the client will be forced to authenticate with one of the selected providers. Make sure you set the appropiate requirements at the <em>Access</em> section since the Authentication System will fallback to the anonymous user if it fails to authenticate. For example: require Access: Role | Authenticated User.'),
'#options' => $this->getAuthOptions(),
'#default_value' => $this->getOption('auth'),
);
}
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
parent::submitOptionsForm($form, $form_state);
if ($form_state->get('section') == 'auth') {
$this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
}
}
/**
* {@inheritdoc}
*/
......@@ -268,6 +337,13 @@ public function collectRoutes(RouteCollection $collection) {
// anyway.
$route->setRequirement('_format', implode('|', $formats + ['html']));
}
// Add authentication to the route if it was set. If no authentication was
// set, the default authentication will be used, which is cookie based by
// default.
$auth = $this->getOption('auth');
if (!empty($auth)) {
$route->setOption('_auth', $auth);
}
}
}
......@@ -348,4 +424,19 @@ public function preview() {
return $this->view->render();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$dependencies += ['module' => []];
$modules = array_map(function ($authentication_provider) {
return $this->authenticationProviders[$authentication_provider];
}, $this->getOption('auth'));
$dependencies['module'] = array_merge($dependencies['module'], $modules);
return $dependencies;
}
}
......@@ -40,7 +40,7 @@ class StyleSerializerTest extends PluginTestBase {
*
* @var array
*/
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language');
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language', 'basic_auth');
/**
* Views used by this test.
......@@ -69,6 +69,39 @@ protected function setUp() {
$this->enableViewsTestModule();
}
/**
* Checks that the auth options restricts access to a REST views display.
*/
public function testRestViewsAuthentication() {
// Assume the view is hidden behind a permission.
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(401);
// Not even logging in would make it possible to see the view, because then
// we are denied based on authentication method (cookie).
$this->drupalLogin($this->adminUser);
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(403);
$this->drupalLogout();
// But if we use the basic auth authentication strategy, we should be able
// to see the page.
$url = $this->buildUrl('test/serialize/auth_with_perm');
$response = \Drupal::httpClient()->get($url, [
'auth' => [$this->adminUser->getUsername(), $this->adminUser->pass_raw],
]);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$headers = $response->getHeaders();
$this->verbose('GET request to: ' . $url .
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
'<hr />Response body: ' . (string) $response->getBody());
$this->assertResponse(200);
}
/**
* Checks the behavior of the Serializer callback paths and row plugins.
*/
......
......@@ -27,7 +27,7 @@ display:
access:
type: perm
options:
perm: 'access content'
perm: 'administer views'
cache:
type: tag
query:
......@@ -149,3 +149,24 @@ display:
type: serializer
row:
type: data_field
rest_export_2:
display_plugin: rest_export
id: rest_export_2
display_title: 'REST export 2'
position: 2
display_options:
display_extenders: { }
auth:
basic_auth: basic_auth
path: test/serialize/auth_with_perm
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:field.storage.node.body'
......@@ -67,6 +67,8 @@ protected function setUp() {
->getMock();
$container->set('router.route_provider', $route_provider);
$container->setParameter('authentication_providers', ['basic_auth' => 'basic_auth']);
$state = $this->getMock('\Drupal\Core\State\StateInterface');
$container->set('state', $state);
......@@ -76,6 +78,12 @@ protected function setUp() {
$container->set('plugin.manager.views.style', $style_manager);
$container->set('renderer', $this->getMock('Drupal\Core\Render\RendererInterface'));
$authentication_collector = $this->getMock('\Drupal\Core\Authentication\AuthenticationCollectorInterface');
$container->set('authentication_collector', $authentication_collector);
$authentication_collector->expects($this->any())
->method('getSortedProviders')
->will($this->returnValue(['basic_auth' => 'data', 'cookie' => 'data']));
\Drupal::setContainer($container);
$this->restExport = RestExport::create($container, array(), "test_routes", array());
......@@ -87,6 +95,9 @@ protected function setUp() {
// Set the style option.
$this->restExport->setOption('style', array('type' => 'serializer'));
// Set the auth option.
$this->restExport->setOption('auth', ['basic_auth']);
$display_manager->expects($this->once())
->method('getDefinition')
->will($this->returnValue(array('id' => 'test', 'provider' => 'test')));
......@@ -132,6 +143,11 @@ public function testRoutesRequirements() {
$this->assertEquals(count($requirements_1), 0, 'First route has no requirement.');
$this->assertEquals(count($requirements_2), 2, 'Views route with rest export had the format and method requirements added.');
// Check auth options.
$auth = $this->routes->get('view.test_view.page_1')->getOption('_auth');
$this->assertEquals(count($auth), 1, 'View route with rest export has an auth option added');
$this->assertEquals($auth[0], 'basic_auth', 'View route with rest export has the correct auth option added');
}
}
<?php
namespace Drupal\views\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Ensures that update hook is run properly for REST Export config.
*
* @group Update
*/
class RestExportAuthUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/rest-export-with-authentication.php',
];
}
/**
* Ensures that update hook is run for views module.
*/
public function testUpdate() {
$this->runUpdates();
// Get particular view.
$view = \Drupal::entityTypeManager()->getStorage('view')->load('rest_export_with_authorization');
$displays = $view->get('display');
$this->assertIdentical($displays['rest_export_1']['display_options']['auth']['basic_auth'], 'basic_auth', 'Basic authentication is set as authentication method.');
}
}
<?php
/**
* @file
* Test fixture for \Drupal\views\Tests\Update\RestExportAuthUpdateTest.
*/
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$config = $connection;
// Set the schema version.
$connection->insert('key_value')
->fields([
'collection' => 'system.schema',
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'basic_auth',
'value' => 'i:8000;',
])
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['rest'] = 0;
$extensions['module']['serialization'] = 0;
$extensions['module']['basic_auth'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
$config = [
'link_domain' => '~',
];
$data = $connection->insert('config')
->fields([
'name' => 'rest.settings',
'data' => serialize($config),
'collection' => ''
])
->execute();
$connection->insert('config')
->fields([
'name' => 'views.view.rest_export_with_authorization',
])
->execute();
$connection->merge('config')
->condition('name', 'views.view.rest_export_with_authorization')
->condition('collection', '')
->fields([
'data' => serialize(Yaml::decode(file_get_contents('core/modules/views/tests/modules/views_test_config/test_views/views.view.rest_export_with_authorization.yml')))
])
->execute();
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- user.role.authenticated
module:
- node
- rest
- user
id: rest_export_with_authorization
label: 'Rest Export'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: role
options:
role:
authenticated: authenticated
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next ›'
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
id: status
table: node_field_data
field: status
relationship: none
group_type: group
admin_label: ''
operator: '='
value: false
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
entity_type: node
entity_field: status
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: 'Rest Export'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.roles
tags: { }
rest_export_1:
display_plugin: rest_export
id: rest_export_1
display_title: 'REST export'
position: 2
display_options:
display_extenders: { }
path: unpublished-content
auth:
basic_auth: basic_auth
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- 'user.node_grants:view'
- user.roles
tags: { }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment