diff --git a/includes/rules.core.inc b/includes/rules.core.inc index cbf926d..2fd66a3 100644 --- a/includes/rules.core.inc +++ b/includes/rules.core.inc @@ -111,7 +111,7 @@ class RulesEntityController extends EntityAPIControllerExportable { // Create an empty configuration, re-set basic keys and import. $config = rules_plugin_factory($export['PLUGIN']); $config->name = $name; - foreach (array('label', 'active', 'weight', 'tags') as $key) { + foreach (array('label', 'active', 'weight', 'tags', 'access_exposed') as $key) { if (isset($export[strtoupper($key)])) { $config->$key = $export[strtoupper($key)]; } @@ -1308,6 +1308,9 @@ abstract class RulesPlugin extends RulesExtendable { if ($modules = $this->dependencies()) { $export_cfg[$this->name]['REQUIRES'] = $modules; } + if (!empty($this->access_exposed)) { + $export_cfg[$this->name]['ACCESS_EXPOSED'] = $this->access_exposed; + }; $export_cfg[$this->name] += $export; return $php ? entity_var_export($export_cfg, $prefix) : entity_var_json_export($export_cfg, $prefix); } diff --git a/modules/data.rules.inc b/modules/data.rules.inc index f97b9a0..7241e83 100644 --- a/modules/data.rules.inc +++ b/modules/data.rules.inc @@ -599,7 +599,8 @@ function rules_condition_data_is_form_alter(&$form, &$form_state, $options, Rule * Provides configuration help for the data_is condition. */ function rules_condition_data_is_help() { - return array('#markup' => t('Compare two data values of the same type with each other.'));} + return array('#markup' => t('Compare two data values of the same type with each other.')); +} /** * Options list callback for condition data_is. diff --git a/modules/rules_core.rules.inc b/modules/rules_core.rules.inc index 72c956e..81c0d0d 100644 --- a/modules/rules_core.rules.inc +++ b/modules/rules_core.rules.inc @@ -286,11 +286,19 @@ function rules_element_invoke_component_features_export(&$export, &$pipe, $modul * Access callback for the invoke component condition/action. */ function rules_element_invoke_component_access_callback($type, $name) { - // Only allow access to the action/condition if the user has access to the - // component. // Cut of the leading 'component_' from the action name. $component = rules_config_load(substr($name, 10)); - return $component && $component->access(); + + if (!$component) { + // Missing component. + return FALSE; + } + // If access is not exposed for this component, default to component access. + if (empty($component->access_exposed)) { + return $component->access(); + } + // Apply the permissions. + return user_access('bypass rules access') || user_access("use Rules component $component->name"); } /** diff --git a/rules.install b/rules.install index 832a557..ce72c81 100644 --- a/rules.install +++ b/rules.install @@ -87,6 +87,13 @@ function rules_schema() { 'length' => 255, 'not null' => FALSE, ), + 'access_exposed' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'Whether to use a permission to control access for using components.', + ), 'data' => array( 'type' => 'blob', 'size' => 'big', @@ -413,3 +420,17 @@ function rules_update_7207() { function rules_update_7208() { // The update system is going to flush all caches anyway, so nothing to do. } + +/** + * Creates a flag that enables a permission for using components. + */ +function rules_update_7209() { + // Create a access exposed flag column. + db_add_field('rules_config', 'access_exposed', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'Whether to use a permission to control access for using components.', + )); +} diff --git a/rules.module b/rules.module index 693284d..7b325e4 100644 --- a/rules.module +++ b/rules.module @@ -995,7 +995,7 @@ function rules_theme() { * Implements hook_permission(). */ function rules_permission() { - return array( + $perms = array( 'administer rules' => array( 'title' => t('Administer rule configurations'), 'description' => t('Administer rule configurations including events, conditions and actions for which the user has sufficient access permissions.'), @@ -1009,6 +1009,30 @@ function rules_permission() { 'title' => t('Access the Rules debug log'), ), ); + + // Fetch all components to generate the access keys. + $conditions['plugin'] = array_keys(rules_filter_array(rules_fetch_data('plugin_info'), 'component', TRUE)); + $conditions['access_exposed'] = 1; + $components = entity_load('rules_config', FALSE, $conditions); + $perms += rules_permissions_by_component($components); + + return $perms; +} + +/** + * Helper function to get all the permissions for components that have access exposed. + */ +function rules_permissions_by_component(array $components = array()) { + $perms = array(); + foreach ($components as $component) { + $perms += array( + "use Rules component $component->name" => array( + 'title' => t('Use Rules component %component', array('%component' => $component->label())), + 'description' => t('Controls access for using the component %component via the provided action or condition. Edit this component.', array('%component' => $component->label(), '@component-edit-url' => url(RulesPluginUI::path($component->name)))), + ), + ); + } + return $perms; } /** diff --git a/tests/rules.test b/tests/rules.test index 59440f7..cd12550 100644 --- a/tests/rules.test +++ b/tests/rules.test @@ -341,6 +341,36 @@ class RulesTestCase extends DrupalWebTestCase { } /** + * Test custom access for using component actions/conditions. + */ + function testRuleComponentAccess() { + // Create a normal user. + $normal_user = $this->drupalCreateUser(); + // Create a role for granting access to the rule component. + $this->normal_role = $this->drupalCreateRole(array(), 'test_role'); + $normal_user->roles[$this->normal_role] = 'test_role'; + user_save($normal_user, array('roles' => $normal_user->roles)); + // Create an 'action set' rule component making use of a permission. + $action_set = rules_action_set(array('node' => array('type' => 'node'))); + $action_set->access_exposed = TRUE; + $action_set->save('rules_test_roles'); + + // Set the global user to be the current one as access is checked for the + // global user. + global $user; + $user = user_load($normal_user->uid); + $this->assertFalse(rules_action('component_rules_test_roles')->access(), 'Authenticated user without the correct role can\'t use the rule component.'); + + // Assign the role that will have permissions for the rule component. + user_role_change_permissions($this->normal_role, array('use Rules component rules_test_roles' => TRUE)); + $this->assertTrue(rules_action('component_rules_test_roles')->access(), 'Authenticated user with the correct role can use the rule component.'); + + // Reset global user to anonyous. + $user = user_load(0); + $this->assertFalse(rules_action('component_rules_test_roles')->access(), 'Anonymous user can\'t use the rule component.'); + } + + /** * Test passing arguments by reference to an action. */ function testPassingByReference() { diff --git a/ui/rules.ui.css b/ui/rules.ui.css index f85212c..b567051 100755 --- a/ui/rules.ui.css +++ b/ui/rules.ui.css @@ -183,6 +183,12 @@ ul.rules-autocomplete .ui-corner-all { -moz-border-radius: 0px; } +/** + * Do not display the hide/show descriptions link above the permissions matrix. + */ +#rules-form-wrapper #edit-settings-access-permissions .compact-link { + display: none; +} /* IE 6 hack for max-height. */ * html ul.rule-autocomplete{ diff --git a/ui/ui.core.inc b/ui/ui.core.inc index 3977dbf..70d75a9 100644 --- a/ui/ui.core.inc +++ b/ui/ui.core.inc @@ -488,11 +488,77 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { '#limit_validation_errors' => array(array('vars')), '#submit' => array('rules_form_submit_rebuild'), ); + if (!empty($this->element->id)) { + // Display a setting to manage access. + $form['settings']['access'] = array( + '#weight' => 50, + ); + $plugin_type = $this->element instanceof RulesActionInterface ? t('action') : t('condition'); + $form['settings']['access']['access_exposed'] = array( + '#type' => 'checkbox', + '#title' => t('Configure access for using this component with a permission.'), + '#default_value' => !empty($this->element->access_exposed), + '#description' => t('By default, the @plugin-type for using this component may be only used by users that have access to configure the component. If checked, access is determined by a permission instead.', array('@plugin-type' => $plugin_type)) + ); + $form['settings']['access']['permissions'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="settings[access][access_exposed]"]' => array('checked' => TRUE), + ), + ), + ); + $form['settings']['access']['permissions']['matrix'] = $this->settingsFormPermissionMatrix(); + } } // TODO: Attach field form thus description. } + /** + * Provides a matrix permission for the component based in the existing roles. + * + * @return + * Form elements with the matrix of permissions for a component. + */ + protected function settingsFormPermissionMatrix() { + $form['#theme'] = 'user_admin_permissions'; + $status = array(); + $options = array(); + + $role_names = user_roles(); + $role_permissions = user_role_permissions($role_names); + $component_permission = rules_permissions_by_component(array($this->element)); + $component_permission_name = key($component_permission); + + $form['permission'][$component_permission_name] = array( + '#type' => 'item', + '#markup' => $component_permission[$component_permission_name]['title'], + ); + $options[$component_permission_name] = ''; + foreach ($role_names as $rid => $name) { + if (isset($role_permissions[$rid][$component_permission_name])) { + $status[$rid][] = $component_permission_name; + } + } + + // Build the checkboxes for each role. + foreach ($role_names as $rid => $name) { + $form['checkboxes'][$rid] = array( + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => isset($status[$rid]) ? $status[$rid] : array(), + '#attributes' => array('class' => array('rid-' . $rid)), + ); + $form['role_names'][$rid] = array('#markup' => check_plain($name), '#tree' => TRUE); + } + + // Attach the default permissions page JavaScript. + $form['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.permissions.js'; + + return $form; + } + public function settingsFormExtractValues($form, &$form_state) { $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state); $this->element->label = $form_values['label']; @@ -532,6 +598,7 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } unset($input['vars']); } + $this->element->access_exposed = isset($form_values['access']['access_exposed']) ? $form_values['access']['access_exposed'] : FALSE; } public function settingsFormValidate($form, &$form_state) { @@ -542,7 +609,12 @@ class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface { } public function settingsFormSubmit($form, &$form_state) { - + if (isset($form_state['values']['settings']['access']) && !empty($this->element->access_exposed)) { + // Save the permission matrix. + foreach ($form_state['values']['settings']['access']['permissions']['matrix']['checkboxes'] as $rid => $value) { + user_role_change_permissions($rid, $value); + } + } } /**