Commit bd8cb79b authored by alexpott's avatar alexpott

Issue #2329485 by damiankloip, dawehner: Allow permissions.yml files to...

Issue #2329485 by damiankloip, dawehner: Allow permissions.yml files to declare 'permission_callbacks' for dynamic permissions.
parent 0a12d85e
......@@ -671,56 +671,6 @@ function template_preprocess_node(&$variables) {
}
}
/**
* Implements hook_permission().
*/
function node_permission() {
$perms = array(
'bypass node access' => array(
'title' => t('Bypass content access control'),
'description' => t('View, edit and delete all content regardless of permission restrictions.'),
'restrict access' => TRUE,
),
'administer content types' => array(
'title' => t('Administer content types'),
'description' => t('Promote, change ownership, edit revisions, and perform other tasks across all content types.'),
'restrict access' => TRUE,
),
'administer nodes' => array(
'title' => t('Administer content'),
'restrict access' => TRUE,
),
'access content overview' => array(
'title' => t('Access the Content overview page'),
'description' => t('Get an overview of <a href="!url">all content</a>.', array('!url' => \Drupal::url('system.admin_content'))),
),
'access content' => array(
'title' => t('View published content'),
),
'view own unpublished content' => array(
'title' => t('View own unpublished content'),
),
'view all revisions' => array(
'title' => t('View all revisions'),
),
'revert all revisions' => array(
'title' => t('Revert all revisions'),
'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
'delete all revisions' => array(
'title' => t('Delete all revisions'),
'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
);
// Generate node permissions for all node types.
foreach (NodeType::loadMultiple() as $type) {
$perms += node_list_permissions($type);
}
return $perms;
}
/**
* Implements hook_ranking().
*/
......@@ -1193,48 +1143,6 @@ function node_node_access(NodeInterface $node, $op, $account) {
return NODE_ACCESS_IGNORE;
}
/**
* Helper function to generate standard node permission list for a given type.
*
* @param $name
* The machine name of the node type.
*
* @return array
* An array of permission names and descriptions.
*/
function node_list_permissions($type) {
// Build standard list of node permissions for this type.
$perms = array(
"create $type->type content" => array(
'title' => t('%type_name: Create new content', array('%type_name' => $type->name)),
),
"edit own $type->type content" => array(
'title' => t('%type_name: Edit own content', array('%type_name' => $type->name)),
),
"edit any $type->type content" => array(
'title' => t('%type_name: Edit any content', array('%type_name' => $type->name)),
),
"delete own $type->type content" => array(
'title' => t('%type_name: Delete own content', array('%type_name' => $type->name)),
),
"delete any $type->type content" => array(
'title' => t('%type_name: Delete any content', array('%type_name' => $type->name)),
),
"view $type->type revisions" => array(
'title' => t('%type_name: View revisions', array('%type_name' => $type->name)),
),
"revert $type->type revisions" => array(
'title' => t('%type_name: Revert revisions', array('%type_name' => $type->name)),
'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
"delete $type->type revisions" => array(
'title' => t('%type_name: Delete revisions', array('%type_name' => $type->name)),
'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
);
return $perms;
}
/**
* Fetches an array of permission IDs granted to the given user ID.
*
......
bypass node access:
title: 'Bypass content access control'
description: 'View edit and delete all content regardless of permission restrictions.'
'restrict access': TRUE
administer content types:
title: 'Administer content types'
description: 'Promote change ownership edit revisions and perform other tasks across all content types.'
'restrict access': TRUE
administer nodes:
title: 'Administer content'
'restrict access': TRUE
access content:
title: 'View published content'
view own unpublished content:
title: 'View own unpublished content'
view all revisions:
title: 'View all revisions'
revert all revisions:
title: 'Revert all revisions'
description: 'Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question or <em>administer nodes</em>.'
delete all revisions:
title: 'Delete all revisions'
description: 'Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question or <em>administer nodes</em>.'
permission_callbacks:
- \Drupal\node\NodePermissions::nodeTypePermissions
- \Drupal\node\NodePermissions::contentPermissions
<?php
/**
* @file
* Contains \Drupal\node\NodePermissions.
*/
namespace Drupal\node;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\Entity\NodeType;
/**
* Defines a class containing permission callbacks.
*/
class NodePermissions {
use StringTranslationTrait;
use UrlGeneratorTrait;
/**
* Returns an array of content permissions.
*
* @return array
*/
public function contentPermissions() {
return array(
'access content overview' => array(
'title' => $this->t('Access the Content overview page'),
'description' => $this->t('Get an overview of <a href="!url">all content</a>.', array('!url' => $this->url('system.admin_content'))),
),
);
}
/**
* Returns an array of node type permissions.
*
* @return array
*/
public function nodeTypePermissions() {
$perms = array();
// Generate node permissions for all node types.
foreach (NodeType::loadMultiple() as $type) {
$perms += $this->buildPermissions($type);
}
return $perms;
}
/**
* Builds a standard list of node permissions for a given type.
*
* @param \Drupal\node\Entity\NodeType $type
* The machine name of the node type.
*
* @return array
* An array of permission names and descriptions.
*/
protected function buildPermissions(NodeType $type) {
$type_id = $type->id();
$type_params = array('%type_name' => $type->label());
return array(
"create $type_id content" => array(
'title' => $this->t('%type_name: Create new content', $type_params),
),
"edit own $type_id content" => array(
'title' => $this->t('%type_name: Edit own content', $type_params),
),
"edit any $type_id content" => array(
'title' => $this->t('%type_name: Edit any content', $type_params),
),
"delete own $type_id content" => array(
'title' => $this->t('%type_name: Delete own content', $type_params),
),
"delete any $type_id content" => array(
'title' => $this->t('%type_name: Delete any content', $type_params),
),
"view $type_id revisions" => array(
'title' => $this->t('%type_name: View revisions', $type_params),
),
"revert $type_id revisions" => array(
'title' => $this->t('%type_name: Revert revisions', $type_params),
'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
"delete $type_id revisions" => array(
'title' => $this->t('%type_name: Delete revisions', $type_params),
'description' => $this->t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
),
);
}
}
......@@ -77,8 +77,8 @@ function testEnableModulesLoad() {
* Tests expected installation behavior of enableModules().
*/
function testEnableModulesInstall() {
$module = 'node';
$table = 'node_access';
$module = 'module_test';
$table = 'module_test';
// Verify that the module does not exist yet.
$this->assertFalse(\Drupal::moduleHandler()->moduleExists($module), "$module module not found.");
......
......@@ -8,6 +8,7 @@
namespace Drupal\user;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
......@@ -44,6 +45,13 @@ class PermissionHandler implements PermissionHandlerInterface {
*/
protected $yamlDiscovery;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* Constructs a new PermissionHandler.
*
......@@ -51,12 +59,15 @@ class PermissionHandler implements PermissionHandlerInterface {
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
*/
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, ControllerResolverInterface $controller_resolver) {
// @todo It would be nice if you could pull all module directories from the
// container.
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
$this->controllerResolver = $controller_resolver;
}
/**
......@@ -94,7 +105,38 @@ public function getPermissions() {
*/
protected function buildPermissionsYaml() {
$all_permissions = array();
$all_callback_permissions = array();
foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
// The top-level 'permissions_callback' is a list of methods in controller
// syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
// should return an array of permissions in the same structure.
if (isset($permissions['permission_callbacks'])) {
foreach ($permissions['permission_callbacks'] as $permission_callback) {
$callback = $this->controllerResolver->getControllerFromDefinition($permission_callback);
if ($callback_permissions = call_user_func($callback)) {
// Add any callback permissions to the array of permissions. Any
// defaults can then get processed below.
foreach ($callback_permissions as $name => $callback_permission) {
if (!is_array($callback_permission)) {
$callback_permission = array(
'title' => $callback_permission,
);
}
$callback_permission += array(
'description' => NULL,
);
$callback_permission['provider'] = $provider;
$all_callback_permissions[$name] = $callback_permission;
}
}
}
unset($permissions['permission_callbacks']);
}
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = array(
......@@ -105,9 +147,11 @@ protected function buildPermissionsYaml() {
$permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
$permission['provider'] = $provider;
}
$all_permissions += $permissions;
}
return $all_permissions;
return $all_permissions + $all_callback_permissions;
}
/**
......
......@@ -44,11 +44,19 @@ class PermissionHandlerTest extends UnitTestCase {
*/
protected $stringTranslation;
/**
* The mocked controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $controllerResolver;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->stringTranslation = $this->getStringTranslationStub();
$this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
}
/**
......@@ -112,7 +120,7 @@ public function testBuildPermissionsModules() {
->method('getModuleList')
->willReturn(array_flip($modules));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
......@@ -180,7 +188,10 @@ public function testBuildPermissionsYaml() {
->method('getModuleList')
->willReturn(array_flip($modules));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
$this->controllerResolver->expects($this->never())
->method('getControllerFromDefinition');
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
......@@ -229,7 +240,7 @@ public function testBuildPermissionsSortPerModule() {
->method('getModuleList')
->willReturn(array_flip($modules));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
......@@ -239,6 +250,143 @@ public function testBuildPermissionsSortPerModule() {
$this->assertEquals(['access_module_a1', 'access_module_a2'], array_keys($actual_permissions));
}
/**
* Tests dynamic callback permissions provided by YML files.
*
* @covers ::__construct
* @covers ::getPermissions
* @covers ::buildPermissions
* @covers ::buildPermissionsYaml
*/
public function testBuildPermissionsYamlCallback() {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$this->moduleHandler->expects($this->once())
->method('getModuleDirectories')
->willReturn(array(
'module_a' => vfsStream::url('modules/module_a'),
'module_b' => vfsStream::url('modules/module_b'),
'module_c' => vfsStream::url('modules/module_c'),
));
$url = vfsStream::url('modules');
mkdir($url . '/module_a');
file_put_contents($url . '/module_a/module_a.permissions.yml',
"permission_callbacks:
- 'Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription'
");
mkdir($url . '/module_b');
file_put_contents($url . '/module_b/module_b.permissions.yml',
"permission_callbacks:
- 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
");
mkdir($url . '/module_c');
file_put_contents($url . '/module_c/module_c.permissions.yml',
"permission_callbacks:
- 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess'
");
$modules = array('module_a', 'module_b', 'module_c');
$extensions = array(
'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
'module_b' => $this->mockModuleExtension('module_b', 'Module b'),
'module_c' => $this->mockModuleExtension('module_c', 'Module c'),
);
$this->moduleHandler->expects($this->any())
->method('getImplementations')
->with('permission')
->willReturn(array());
$this->moduleHandler->expects($this->any())
->method('getModuleList')
->willReturn(array_flip($modules));
$this->controllerResolver->expects($this->at(0))
->method('getControllerFromDefinition')
->with('Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription')
->willReturn(array(new TestPermissionCallbacks(), 'singleDescription'));
$this->controllerResolver->expects($this->at(1))
->method('getControllerFromDefinition')
->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
$this->controllerResolver->expects($this->at(2))
->method('getControllerFromDefinition')
->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess')
->willReturn(array(new TestPermissionCallbacks(), 'titleDescriptionRestrictAccess'));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
$actual_permissions = $this->permissionHandler->getPermissions();
$this->assertPermissions($actual_permissions);
}
/**
* Tests a YAML file containing both static permissions and a callback.
*/
public function testPermissionsYamlStaticAndCallback() {
vfsStreamWrapper::register();
$root = new vfsStreamDirectory('modules');
vfsStreamWrapper::setRoot($root);
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$this->moduleHandler->expects($this->once())
->method('getModuleDirectories')
->willReturn(array(
'module_a' => vfsStream::url('modules/module_a'),
));
$url = vfsStream::url('modules');
mkdir($url . '/module_a');
file_put_contents($url . '/module_a/module_a.permissions.yml',
"'access module a':
title: 'Access A'
description: 'bla bla'
permission_callbacks:
- 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
");
$modules = array('module_a');
$extensions = array(
'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
);
$this->moduleHandler->expects($this->any())
->method('getImplementations')
->with('permission')
->willReturn(array());
$this->moduleHandler->expects($this->any())
->method('getModuleList')
->willReturn(array_flip($modules));
$this->controllerResolver->expects($this->once())
->method('getControllerFromDefinition')
->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
$actual_permissions = $this->permissionHandler->getPermissions();
$this->assertCount(2, $actual_permissions);
$this->assertEquals($actual_permissions['access module a']['title'], 'Access A');
$this->assertEquals($actual_permissions['access module a']['provider'], 'module_a');
$this->assertEquals($actual_permissions['access module a']['description'], 'bla bla');
$this->assertEquals($actual_permissions['access module b']['title'], 'Access B');
$this->assertEquals($actual_permissions['access module b']['provider'], 'module_a');
$this->assertEquals($actual_permissions['access module b']['description'], 'bla bla');
}
/**
* Checks that the permissions are like expected.
*
......@@ -276,3 +424,32 @@ public function setSystemRebuildModuleData(array $extensions) {
}
}
class TestPermissionCallbacks {
public function singleDescription() {
return array(
'access_module_a' => 'single_description'
);
}
public function titleDescription() {
return array(
'access module b' => array(
'title' => 'Access B',
'description' => 'bla bla',
),
);
}
public function titleDescriptionRestrictAccess() {
return array(
'access_module_c' => array(
'title' => 'Access C',
'description' => 'bla bla',
'restrict access' => TRUE,
),
);
}
}
......@@ -61,7 +61,7 @@ services:
- { name: backend_overridable }
user.permissions:
class: Drupal\user\PermissionHandler
arguments: ['@module_handler', '@string_translation']
arguments: ['@module_handler', '@string_translation', '@controller_resolver']
parameters:
user.tempstore.expire: 604800
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