...
 
Commits (9)
$Id$
Content Access Module
-----------------------
......@@ -15,7 +14,11 @@ In particular
as well as with flexible Access Control Lists.
* it trys to reuse existing functionality instead of reimplementing it. So one can install the ACL
module and set per user access control settings per node.
Furthermore the module provides conditions and actions for the workflow-ng module, which allows one
to configure even rule-based access permissions.
* it optimizes the written node grants, so that only really necessary grants are written.
* it takes access control as important as it is. E.g. the module has a bunch of simpletests to ensure
everything is working right.
So the module is simple to use, but can be configured to provide really fine-grained permissions!
......@@ -46,3 +49,16 @@ to the latest development snapshot of drupal 5.x before.
You can find it here: http://drupal.org/project/drupal
If drupal 5.2 is already released at the time you read that, it will also contain the fix.
Running multiple node access modules on a site (Advanced!)
-----------------------------------------------------------
A drupal node access module can only grant access to content nodes, but not deny it. So if you
are using multiple node access modules, access will be granted to a node as soon as one of the
module grants access to it.
However you can influence the behaviour by changing the priority of the content access module as
drupal applies *only* the grants with the highest priority. So if content access has the highest
priority *alone*, only its grants will be applied.
By default node access modules use priority 0.
; $Id$
name = Content Access
description = Provides flexible content access control
package = Access control
\ No newline at end of file
<?php
// $Id$
/*
* Implementation of hook_install
*/
......
<?php
// $Id$
if (module_exists('workflow_ng')) {
include_once(drupal_get_path('module', 'content_access') .'/content_access.workflow_ng.inc');
......@@ -72,7 +71,7 @@ function content_access_page($nid) {
foreach (array('view', 'update', 'delete') as $i) {
$defaults[$i] = content_access_per_node_setting($i, $node);
}
$form = content_access_page_form($defaults, $node);
$form['node'] = array('#type' => 'value', '#value' => $node);
......@@ -87,30 +86,30 @@ function content_access_page($nid) {
/**
* Builds per node setting page form without requiring a node. Used by
* content_access_action_set_node_permissions_form().
*
*
* @param $defaults
* Array of defaults for view/update/delete checkboxes.
* @param $node
* Optional node for ACL.
*/
function content_access_page_form($defaults = array(), $node = FALSE) {
// Make sure defaults array is full.
foreach (array('view', 'update', 'delete') as $op) {
if (!isset($defaults[$op])) $defaults[$op] = array();
}
$roles = content_access_get_roles_and_author();
$form['settings'] = array(
'#type' => 'fieldset',
'#type' => 'fieldset',
'#title' => t('Role access control settings'),
'#collapsible' => TRUE,
);
if (!$node) {
$form['settings']['#description'] = t('Warning: No defaults are set; be sure to fill out all boxes appropriately.');
}
drupal_add_css(drupal_get_path('module', 'content_access') . '/content_access.css');
$form['settings']['view'] = array('#type' => 'checkboxes',
'#prefix' => '<div class="content_access-div">',
......@@ -139,7 +138,7 @@ function content_access_page_form($defaults = array(), $node = FALSE) {
if (module_exists('acl') && $node) {
// This is disabled when there is no node passed.
$form['acl'] = array(
'#type' => 'fieldset',
'#type' => 'fieldset',
'#title' => t('User access control lists'),
'#description' => t('These settings allow you to grant access to specific users.'),
'#collapsible' => TRUE,
......@@ -149,11 +148,11 @@ function content_access_page_form($defaults = array(), $node = FALSE) {
$acl_id = acl_get_id_by_name('content_access', $op .'_'. $node->nid);
if (!$acl_id) {
// Create one:
$acl_id = acl_create_new_acl('content_access', $op .'_'. $node->nid, content_access_get_settings('priority', $node->type));
acl_node_add_acl($node->nid, $acl_id, $op == 'view', $op == 'update', $op == 'delete');
$acl_id = acl_create_new_acl('content_access', $op .'_'. $node->nid);
}
acl_node_add_acl($node->nid, $acl_id, $op == 'view', $op == 'update', $op == 'delete', content_access_get_settings('priority', $node->type));
$form['acl'][$op] = acl_edit_form($acl_id, 'Grant '. $op .' access');
$form['acl'][$op]['#collapsed'] = !isset($_POST['acl'][$op]['add_button']) && !isset($_POST['acl'][$op]['delete_button']);
$form['acl'][$op]['#collapsed'] = !isset($_POST['acl'][$op]['add_button']) && !isset($_POST['acl'][$op]['delete_button']);
}
}
return $form;
......@@ -171,12 +170,14 @@ function content_access_page_submit($form_id, $form_values) {
acl_save_form($form_values['acl'][$op]);
}
}
// Save per-node settings.
content_access_save_per_node_settings($node, $settings);
// Apply new settings.
node_access_acquire_grants($node);
cache_clear_all();
drupal_set_message('Your changes have been saved.');
}
......@@ -200,7 +201,7 @@ function content_access_admin_settings($type) {
);
// Defaults:
$form['defaults'] = array(
'#type' => 'fieldset',
'#type' => 'fieldset',
'#title' => t('Default access control settings'),
'#collapsible' => TRUE,
'#description' => t('If per node settings are available, the default settings will be overridden by them.'),
......@@ -268,22 +269,29 @@ function content_access_admin_settings_submit($form_id, $form_values) {
// Mass update all nodes that use default settings.
if (content_access_get_settings('per_node', $form_values['type']) && $per_node_old) {
$sql = "SELECT n.nid FROM {node} n LEFT JOIN {content_access} na ON na.nid = n.nid
$sql = "SELECT n.nid FROM {node} n LEFT JOIN {content_access} na ON na.nid = n.nid
WHERE type = '%s' AND na.nid IS NULL";
}
else {
$sql = "SELECT n.nid FROM {node} n WHERE type = '%s'";
}
// If per node has been disabled and we use the ACL integration, we have to remove possible ACLs now.
$remove_acls = !content_access_get_settings('per_node', $form_values['type']) && $per_node_old && module_exists('acl');
$result = db_query($sql, $form_values['type']);
while ($node = db_fetch_object($result)) {
if ($remove_acls) {
acl_node_clear_acls($node->nid, 'content_access');
}
node_access_acquire_grants(node_load($node->nid));
}
cache_clear_all();
drupal_set_message('Your changes have been saved.');
}
/*
* Implementation of hook_node_access_records()
*
*
* @param $optimize
* If the grants should be returned optimized.
*/
......@@ -364,7 +372,7 @@ function content_access_disabling($set = NULL) {
/*
* Returns the content_access' settings.
*
*
* @param $return
* One of the content_access_available_settings(), e.g. 'view' or 'per_node'.
* @param $type
......@@ -448,7 +456,7 @@ function content_access_get_permission_access($perm) {
function content_access_get_roles_and_author() {
static $roles;
if (!isset($roles)) {
$roles = array('author' => t('author')) + user_roles();
$roles = array('author' => t('author')) + array_map('filter_xss_admin', user_roles());
}
return $roles;
}
......@@ -461,7 +469,6 @@ function content_access_get_default_grant($node) {
if (!isset($defaults[$node->type])) {
$grants = array(); //apply the defaults
$roles = content_access_get_roles_and_author();
foreach (array('view', 'update', 'delete') as $op) {
foreach (content_access_get_settings($op, $node->type) as $rid) {
......@@ -495,7 +502,7 @@ function content_access_proccess_grant($grant, $rid, $node) {
/*
* Returns the per node role settings. If no per node settings are available, it will return the
* default settings.
*
*
* @param $op
* One of view, update or delete.
* @param $node
......@@ -507,7 +514,7 @@ function content_access_proccess_grant($grant, $rid, $node) {
*/
function content_access_per_node_setting($op, $node, $settings = NULL) {
static $grants = array();
if (isset($settings)) {
//update settings cache
$grants[$node->nid] = $settings;
......@@ -535,9 +542,9 @@ function content_access_save_per_node_settings($node, $settings) {
/*
* Gets the per node settings of a node.
*
*
* @note
* This function won't apply defaults, so if there are no other settings
* This function won't apply defaults, so if there are no other settings
* it will return an empty array.
*/
function content_access_get_per_node_settings($node) {
......@@ -550,19 +557,19 @@ function content_access_get_per_node_settings($node) {
/*
* Removes grants that doesn't change anything.
*
*
* @note
* The grants are compared with the normal access control settings.
*/
function content_access_optimize_grants(&$grants, $node) {
//populate $view, $update and $delete with roles, that have access
foreach (array('view', 'update', 'delete') as $op) {
$$op = array();
$$op = array();
}
foreach ($grants as $key => $grant) {
foreach (array('view', 'update', 'delete') as $op) {
if ($grant['grant_'. $op]) {
${$op}[] = $key;
${$op}[] = $key;
}
}
}
......@@ -622,18 +629,21 @@ function content_access_node_type($op, $info) {
}
/**
* Implementation of hook_node_access_explain
* Implementation of hook_node_access_explain().
*/
function content_access_node_access_explain($row) {
static $interpretations = array();
static $roles;
if (!isset($roles)) {
$roles = user_roles();
}
if (!$row->gid && $row->realm == 'content_access_rid') {
return t('Content access: No access is granted.');
}
switch ($row->realm) {
case 'content_access_author':
$interpretations[$row->gid] = t('Content access: author of the content can access');
break;
case 'content_access_author':
return t('Content access: author of the content can access');
case 'content_access_rid':
$roles = user_roles();
$interpretations[$row->gid] = t('Content access: ') . $roles[$row->gid] . t(' can access');
break;
return t('Content access: %role can access', array('%role' => $roles[$row->gid]));
}
return $interpretations[$row->gid];
}
\ No newline at end of file
This diff is collapsed.
<?php
/**
* @file
* Automatd SimpleTest Case for using content access module with acl module
*/
require_once(drupal_get_path('module', 'content_access') .'/tests/content_access_test_help.php');
class ContentAccessACLTestCase extends ContentAccessTestCase {
var $node;
/**
* Implementation of get_info() for information
*/
function get_info() {
return array(
'name' => t('Content Access Module with ACL Module Tests'),
'desc' => t('Various tests to check the combination of content access and ACL module.'),
'group' => 'Content Access',
);
}
/**
* Setup configuration before each test
*/
function setUp() {
parent::setUp();
// Create test nodes
$this->node = $this->createNode();
}
/**
* Test Viewing accessibility with permissions for single users
*/
function testViewAccess() {
// Enable ACL module
// Exit test if module could not be enabled
if (!$this->aclModuleEnable()) {
$this->pass('No ACL module present, skipping test');
return;
}
// Restrict access to this content type (access is only allowed for the author)
// Enable per node access control
$access_permissions = array(
'view[author]' => TRUE,
'view[1]' => FALSE,
'view[2]' => FALSE,
'per_node' => TRUE,
);
$this->changeAccessContentType($access_permissions);
// Allow access for test user
$edit = array(
'acl[view][add]' => $this->test_user->name,
);
$this->drupalPostRequest('node/'. $this->node->nid .'/access', $edit, 'Add User');
$this->postToCurrentPage(array(), 'Submit');
// Logout admin, try to access the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid));
$this->assertText(t('Access denied'), 'node is not viewable');
// Login test user, view access should be allowed now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid));
$this->assertNoText(t('Access denied'), 'node is viewable');
// Login admin and disable per node access
$this->drupalGet(url('logout'));
$this->drupalLoginUser($this->admin_user);
$this->changeAccessPerNode(FALSE);
// Logout admin, try to access the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid));
$this->assertText(t('Access denied'), 'node is not viewable');
// Login test user, view access should be denied now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid));
$this->assertText(t('Access denied'), 'node is not viewable');
}
/**
* Test Editing accessibility with permissions for single users
*/
function testEditAccess() {
// Enable ACL module
// Exit test if module could not be enabled
if (!$this->aclModuleEnable()) {
$this->pass('No ACL module present, skipping test');
return;
}
// Enable per node access control
$this->changeAccessPerNode();
// Allow edit access for test user
$edit = array(
'acl[update][add]' => $this->test_user->name,
);
$this->drupalPostRequest('node/'. $this->node->nid .'/access', $edit, 'acl[update][add_button]');
$this->postToCurrentPage(array(), 'Submit');
// Logout admin, try to edit the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid .'/edit'));
$this->assertText(t('Access denied'), 'node is not editable');
// Login test user, edit access should be allowed now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid .'/edit'));
$this->assertNoText(t('Access denied'), 'node is editable');
// Login admin and disable per node access
$this->drupalGet(url('logout'));
$this->drupalLoginUser($this->admin_user);
$this->changeAccessPerNode(FALSE);
// Logout admin, try to edit the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid .'/edit'));
$this->assertText(t('Access denied'), 'node is not editable');
// Login test user, edit access should be denied now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid .'/edit'));
$this->assertText(t('Access denied'), 'node is not editable');
}
/**
* Test Deleting accessibility with permissions for single users
*/
function testDeleteAccess() {
// Enable ACL module
// Exit test if module could not be enabled
if (!$this->aclModuleEnable()) {
$this->pass('No ACL module present, skipping test');
return;
}
// Enable per node access control
$this->changeAccessPerNode();
// Allow delete access for test user
$edit = array(
'acl[delete][add]' => $this->test_user->name,
);
$this->drupalPostRequest('node/'. $this->node->nid .'/access', $edit, 'acl[delete][add_button]');
$this->postToCurrentPage(array(), 'Submit');
// Logout admin, try to delete the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid .'/delete'));
$this->assertText(t('Access denied'), 'node is not deletable');
// Login test user, delete access should be allowed now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid .'/delete'));
$this->assertNoText(t('Access denied'), 'node is deletable');
// Login admin and disable per node access
$this->drupalGet(url('logout'));
$this->drupalLoginUser($this->admin_user);
$this->changeAccessPerNode(FALSE);
// Logout admin, try to delete the node anonymously
$this->drupalGet(url('logout'));
$this->drupalGet(url('node/'. $this->node->nid .'/delete'));
$this->assertText(t('Access denied'), 'node is not deletable');
// Login test user, delete access should be denied now
$this->drupalLoginUser($this->test_user);
$this->drupalGet(url('node/'. $this->node->nid .'/delete'));
$this->assertText(t('Access denied'), 'node is not deletable');
}
/**
* Enables the ACL module and returns TRUE on success
*/
function aclModuleEnable() {
if (module_exists('acl')) {
return TRUE;
}
module_enable(array('acl'));
if (module_exists('acl')) {
module_disable(array('acl'));
// Make sure we use drupalModuleEnable(), so the module is disabled again afterwards.
$this->drupalModuleEnable('acl');
return TRUE;
}
return FALSE;
}
}
<?php
/**
* @file
* Helper class with auxiliary functions for content access module tests
*/
class ContentAccessTestCase extends DrupalTestCase {
var $test_user;
var $admin_user;
var $content_type_name;
var $url_content_type_name;
var $node1;
var $node2;
/**
* Preparation work that is done before each test.
* Test users, content types, nodes etc. are created.
*/
function setUp() {
parent::setUp();
// Create test user with seperate role
$this->test_user = $this->drupalCreateUserRolePerm();
// Create admin user
$this->admin_user = $this->drupalCreateUserRolePerm(array('administer content types', 'grant content access', 'grant own content access', 'administer nodes'));
$this->drupalLoginUser($this->admin_user);
// Create test content type
$this->content_type_name = strtolower($this->randomName(5));
$edit = array(
'name' => $this->content_type_name,
'type' => $this->content_type_name,
);
$this->drupalPostRequest('admin/content/types/add', $edit, 'op');
$this->assertWantedRaw(t('The content type %type has been added.', array ('%type' => $this->content_type_name)), 'Test content type was added successfully');
$this->url_content_type_name = str_replace('_', '-', $this->content_type_name);
}
/**
* Clear up work that is done after each test.
* Users get deleted automatically, so we just have to delete the content type.
* (nodes get deleted automatically, too)
*/
function tearDown() {
// Login admin and delete test content type
$this->drupalGet(url('logout'));
$this->drupalLoginUser($this->admin_user);
$this->drupalPostRequest('admin/content/types/'. $this->url_content_type_name .'/delete', array(), 'op');
parent::tearDown();
}
/**
* Creates a node for testing purposes
* @return the node object
*/
function createNode() {
$title = $this->randomName(10);
$edit = array(
'title' => $title,
'body' => $this->randomName(20),
);
$this->drupalPostRequest('node/add/'. $this->url_content_type_name, $edit, 'Submit');
$this->assertWantedRaw(t('Your %content has been created.', array ('%content' => $this->content_type_name)), 'Test node was added successfully');
return node_load(array('title' => $title, 'type' => $this->content_type_name));
}
/**
* Change access permissions for a content type
*/
function changeAccessContentType($access_settings) {
$this->drupalPostRequest('admin/content/types/'. $this->url_content_type_name .'/access', $access_settings, 'Submit');
$this->assertText(t('Your changes have been saved.'), 'access rules of content type were updated successfully');
}
/**
* Change access permissions for a content type by a given keyword (view, update or delete)
* for the role of the user
*/
function changeAccessContentTypeKeyword($keyword, $access = TRUE) {
$user = $this->test_user;
$roles = $user->roles;
end($roles);
$access_settings = array(
$keyword .'['. key($roles) .']' => $access,
);
$this->changeAccessContentType($access_settings);
}
/**
* Change access permission for a node
*/
function changeAccessNode($node, $access_settings) {
$this->drupalPostRequest('node/'. $node->nid .'/access', $access_settings, 'Submit');
$this->assertText(t('Your changes have been saved.'), 'access rules of node were updated successfully');
}
/**
* Change access permissions for a node by a given keyword (view, update or delete)
*/
function changeAccessNodeKeyword($node, $keyword, $access = TRUE) {
$user = $this->test_user;
$roles = $user->roles;
end($roles);
$access_settings = array(
$keyword .'['. key($roles) .']' => $access,
);
$this->changeAccessNode($node, $access_settings);
}
/**
* Change the per node access setting for a content type
*/
function changeAccessPerNode($access = TRUE) {
$access_permissions = array(
'per_node' => $access,
);
$this->changeAccessContentType($access_permissions);
}
/**
* Replacement for drupalPostRequest() if we don't want to reload a path
*/
function postToCurrentPage($edit = array(), $submit) {
foreach ($edit as $field_name => $field_value) {
$ret = $this->_browser->setFieldByName($field_name, $field_value)
|| $this->_browser->setFieldById("edit-$field_name", $field_value);
$this->assertTrue($ret, " [browser] Setting $field_name=\"$field_value\"");
}
$ret = $this->_browser->clickSubmit(t($submit)) || $this->_browser->clickSubmitById($submit) || $this->_browser->clickSubmitByName($submit) || $this->_browser->clickImageByName($submit);
$this->assertTrue($ret, ' [browser] POST by click on ' . t($submit));
$this->_content = $this->_browser->getContent();
}
/**
* Stores the current page in the files directory, so it can be viewed by the developer.
* Useful for debugging code.
*/
function debugCurrentPage() {
static $count = 0;
$count++;
$path = 'tests';
if (!file_check_directory($path, FILE_CREATE_DIRECTORY)) {
return FALSE; //unable to create directory
}
$filepath = file_create_filename("page-$count.htm", $path);
file_put_contents($filepath, $this->drupalGetContent());
echo '<a href="'. $filepath .'">created debug file '. $count .'</a><br/>';
//echo l("created debug file $count", $filepath);
}
}