diff --git a/examples.module b/examples.module index efdf09b..6c7b024 100644 --- a/examples.module +++ b/examples.module @@ -41,6 +41,7 @@ function examples_toolbar() { 'dbtng_example' => 'dbtng_example', 'email_example' => 'email_example.description', 'field_example' => 'field_example.description', + 'field_permission_example' => 'field_permission_example.description', 'js_example' => 'js_example.info', 'node_type_example' => 'config_node_type_example.description', 'page_example' => 'page_example_description', diff --git a/field_permission_example/css/field-permission-description.css b/field_permission_example/css/field-permission-description.css new file mode 100644 index 0000000..a366cc5 --- /dev/null +++ b/field_permission_example/css/field-permission-description.css @@ -0,0 +1,3 @@ +/** + * Field Permssions Example CSS + */ diff --git a/field_permission_example/css/field_permission_example.css b/field_permission_example/css/field_permission_example.css new file mode 100644 index 0000000..bf5a999 --- /dev/null +++ b/field_permission_example/css/field_permission_example.css @@ -0,0 +1,20 @@ +/** + * @file + * CSS for Field Example. + */ +.stickynote { + background: #fefabc; + padding: 0.8em; + font-family: cursive; + font-size: 1.1em; + color: 1000; + width: 15em; + -moz-transform: rotate(2deg); + -webkit-transform: rotate(2deg); + -o-transform: rotate(2deg); + -ms-transform: rotate(2deg); + transform: rotate(2deg); + box-shadow: 0px 4px 6px #333; + -moz-box-shadow: 0px 4px 6px #333; + -webkit-box-shadow: 0px 4px 6px #333; +} diff --git a/field_permission_example/field_permission_example.info.yml b/field_permission_example/field_permission_example.info.yml new file mode 100644 index 0000000..d59c28f --- /dev/null +++ b/field_permission_example/field_permission_example.info.yml @@ -0,0 +1,8 @@ +name: Field Permission Example +type: module +description: An example module that creates a field and puts access control over it. +package: Example modules +version: 8.x-1.x +core: 8.x +dependencies: + - examples diff --git a/field_permission_example/field_permission_example.libraries.yml b/field_permission_example/field_permission_example.libraries.yml new file mode 100644 index 0000000..9b4e619 --- /dev/null +++ b/field_permission_example/field_permission_example.libraries.yml @@ -0,0 +1,10 @@ +fieldnote_sticky: + version: 1.x + css: + theme: + css/field_permission_example.css: {} +field_permissions_description: + version: 1.x + css: + theme: + css/field-permission-description.css: {} diff --git a/field_permission_example/field_permission_example.links.menu.yml b/field_permission_example/field_permission_example.links.menu.yml new file mode 100644 index 0000000..ce9a56b --- /dev/null +++ b/field_permission_example/field_permission_example.links.menu.yml @@ -0,0 +1,3 @@ +field_permission_example.description: + title: Field Permission Example + route_name: field_permission_example.description diff --git a/field_permission_example/field_permission_example.module b/field_permission_example/field_permission_example.module new file mode 100644 index 0000000..0cd7d84 --- /dev/null +++ b/field_permission_example/field_permission_example.module @@ -0,0 +1,201 @@ +hasPermission(). We also give special + * edit access to users with the 'bypass node access', + * 'administer content types' permissions, defined by the node module, + * and the 'administer the fieldnote field' we define for the module. + * + * One tricky part is that our field won't always be attached to + * nodes. It could be attached to any type of entity. Fortunately, + * most content entities implement EntityOwnerInterface, which gives us + * a way to check this. An exception to this is the User entity; here, we + * just check to see that the account name matches that of $account. We + * can get the entity itself by calling $items->getEntity(), since these + * "know" what entity they belong to. + * + * In a real application, we'd have use-case specific permissions + * which might be more complex than these. Or perhaps simpler. + * + * You can see a more complex field implementation in + * field_example.module. + * + * @see field_example + * @see field_example.module + * @see field_types + * @see field + */ + +// Use statements to support hook_entity_field_access. +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Access\AccessResult; + +// Interfaces used by entities to declare "ownership". +use Drupal\user\EntityOwnerInterface; +use Drupal\user\UserInterface; + +// Use statements for hook_entity_test_access. +use Drupal\Core\Entity\EntityInterface; + +/** + * Implements hook_entity_field_access(). + * + * We want to make sure that fields aren't being seen or edited + * by those who shouldn't. + */ +function field_permission_example_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + // Find out what field we're looking at. If it isn't + // our sticky note widget, tell Drupal we don't care about its access. + if ($field_definition->getType() != 'field_permission_example_fieldnote') { + return AccessResult::neutral(); + } + + // First we'll check if the user has the 'superuser' + // permissions that node provides. This way administrators + // will be able to administer the content types. + if ($account->hasPermission('bypass node access')) { + drupal_set_message(t('User can bypass node access.')); + return AccessResult::allowed(); + } + if ($account->hasPermission('administer content types', $account)) { + drupal_set_message(t('User can administer content types.')); + return AccessResult::allowed(); + } + if ($account->hasPermission('administer the fieldnote field', $account)) { + drupal_set_message(t('User can administer this field.')); + return AccessResult::allowed(); + } + + // For anyone else, it depends on the desired operation. + if ($operation == 'view' and $account->hasPermission('view any fieldnote')) { + drupal_set_message(t('User can view any field note.')); + return AccessResult::allowed(); + } + + if ($operation == 'edit' and $account->hasPermission('edit any fieldnote')) { + drupal_set_message(t('User can edit any field note.')); + return AccessResult::allowed(); + } + + // At this point, we need to know if the user "owns" the entity we're attached to. + // If it's a user, we'll use the account name to test. Otherwise rely on the entity implementing the + // the EntityOwnerInterface. Anything else can't be owned, and we'll refuse access. + if ($items) { + $entity = $items->getEntity(); + if ((($entity instanceof EntityOwnerInterface) and + $entity->getOwner()->getAccountName() == $account->getAccountName()) or + (($entity instanceof UserInterface) and + $entity->name->value == $account->getAccountName()) + ) { + if ($operation == 'view' and $account->hasPermission('view own fieldnote')) { + drupal_set_message(t('User can view their own field note.')); + return AccessResult::allowed(); + } + if ($operation == 'edit' and $account->hasPermission('edit own fieldnote')) { + drupal_set_message(t('User can edit their own field note.')); + return AccessResult::allowed(); + } + } + } + // Anything else on this field is forbidden. + return AccessResult::forbidden(); +} + + +/** + * Implements hook_ENTITY_TYPE_access(). + * + * Note: this routine is added so we can more easily test our access code. Core + * defines an entity_test entity that is used for testing fields in core. We add + * this routine to make the entity_test entity editable by our tests. + */ +function field_permission_example_entity_test_access(EntityInterface $entity, $operation, AccountInterface $account, $langcode) { + if ($operation == 'edit') { + $perms = [ + 'administer the fieldnote field', + 'edit any fieldnote', + 'edit own fieldnote', + ]; + foreach ($perms as $perm) { + if ($account->hasPermission($perm)) { + return AccessResult::allowed(); + } + } + } + return AccessResult::neutral(); +} +/** + * @} End of "defgroup field_permission_example". + */ + +/** + * Implements hook_theme(). + * + * Since we have a lot to explain, we're going to use Twig to do it. + */ +function field_permission_example_theme() { + return [ + 'field_permission_description' => [ + 'template' => 'description', + 'variables' => [ + 'admin_link' => NULL, + ], + ], + ]; +} diff --git a/field_permission_example/field_permission_example.permissions.yml b/field_permission_example/field_permission_example.permissions.yml new file mode 100644 index 0000000..c70eb2b --- /dev/null +++ b/field_permission_example/field_permission_example.permissions.yml @@ -0,0 +1,11 @@ +# Permissions for the field_permission_example module +'view own fieldnote': + title: View own fieldnote +'edit own fieldnote': + title: Edit own fieldnote +'view any fieldnote': + title: View any fieldnote +'edit any fieldnote': + title: Edit any fieldnote +'administer the fieldnote field': + title: Administer settings for the fieldnote field. diff --git a/field_permission_example/field_permission_example.routing.yml b/field_permission_example/field_permission_example.routing.yml new file mode 100644 index 0000000..dd1c34a --- /dev/null +++ b/field_permission_example/field_permission_example.routing.yml @@ -0,0 +1,7 @@ +field_permission_example.description: + path: '/examples/field_permission_example' + defaults: + _title: 'Field Permission Example' + _controller: '\Drupal\field_permission_example\Controller\FieldPermissionExampleController::description' + requirements: + _permission: 'access content' diff --git a/field_permission_example/src/Controller/FieldPermissionExampleController.php b/field_permission_example/src/Controller/FieldPermissionExampleController.php new file mode 100644 index 0000000..be6a2c5 --- /dev/null +++ b/field_permission_example/src/Controller/FieldPermissionExampleController.php @@ -0,0 +1,35 @@ +l($this->t('the permissions admin page'), $url); + + $build = [ + 'description' => [ + '#theme' => 'field_permission_description', + '#admin_link' => $permissions_admin_link, + ], + ]; + return $build; + } + +} diff --git a/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php b/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php new file mode 100644 index 0000000..3e737d8 --- /dev/null +++ b/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php @@ -0,0 +1,54 @@ + $item) { + $elements[$delta] = array( + // We wrap the fieldnote content up in a div tag. + '#type' => 'html_tag', + '#tag' => 'div', + // This text is auto-XSS escaped. See docs for @RenderElement("html_tag"). + '#value' => $item->value, + // Let's give the note a nice sticky-note CSS appearance. + '#attributes' => array( + 'class' => 'stickynote', + ), + // ..And this is the CSS for the stickynote. + '#attached' => array( + 'library' => array('field_permission_example/fieldnote_sticky'), + ), + ); + } + + return $elements; + } + +} diff --git a/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php b/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php new file mode 100644 index 0000000..c34ef68 --- /dev/null +++ b/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php @@ -0,0 +1,60 @@ + array( + 'value' => array( + 'type' => 'text', + 'size' => 'normal', + 'not null' => FALSE, + ), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $value = $this->get('value')->getValue(); + return $value === NULL || $value === ''; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['value'] = DataDefinition::create('string') + ->setLabel(t('Field Note')); + + return $properties; + } + +} diff --git a/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php b/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php new file mode 100644 index 0000000..e4a02f9 --- /dev/null +++ b/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php @@ -0,0 +1,40 @@ +value) ? $items[$delta]->value : ''; + $element += array( + '#type' => 'textarea', + '#default_value' => $value, + ); + return array('value' => $element); + } + +} diff --git a/field_permission_example/src/Tests/FieldNoteItemTest.php b/field_permission_example/src/Tests/FieldNoteItemTest.php new file mode 100644 index 0000000..84e13ba --- /dev/null +++ b/field_permission_example/src/Tests/FieldNoteItemTest.php @@ -0,0 +1,280 @@ +container->get('entity_type.manager'); + + // Set up our entity_type and user type for our new field: + $type_manager + ->getStorage('field_storage_config') + ->create([ + 'field_name' => 'field_fieldnote', + 'entity_type' => 'entity_test', + 'type' => 'field_permission_example_fieldnote', + ])->save(); + + $type_manager + ->getStorage('field_config') + ->create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_fieldnote', + 'bundle' => 'entity_test', + ])->save(); + + // Create a form display for the default form mode, and + // add our field type. + $type_manager + ->getStorage('entity_form_display') + ->create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]) + ->setComponent('field_fieldnote', [ + 'type' => 'field_permission_example_widget', + ]) + ->save(); + + // Now do this for the user type. + $type_manager + ->getStorage('field_storage_config') + ->create([ + 'field_name' => 'user_fieldnote', + 'entity_type' => 'user', + 'type' => 'field_permission_example_fieldnote', + ])->save(); + + $type_manager + ->getStorage('field_config') + ->create([ + 'entity_type' => 'user', + 'field_name' => 'user_fieldnote', + 'bundle' => 'user', + ])->save(); + + // Fetch a form display for a user. + // Most likely, this will already exist, so check as Core does. + // @see https://api.drupal.org/api/drupal/core%21includes%21entity.inc/function/entity_get_form_display/8 + $entity_form_display + = $type_manager + ->getStorage('entity_form_display') + ->load('user.user.default'); + if (empty($entity_form_display)) { + $entity_form_display + = $type_manager + ->getStorage('entity_form_display') + ->create([ + 'targetEntityType' => 'user', + 'bundle' => 'user', + 'mode' => 'default', + 'status' => TRUE, + ]); + } + // And add our fancy field to that display: + $entity_form_display->setComponent('field_fieldnote', [ + 'type' => 'field_permission_example_widget', + ])->save(); + + } + + /** + * Test entity fields of the field_permission_example_fieldnote field type. + */ + public function testFieldNoteItem() { + // Verify entity creation. + $type_manager = $this->container->get('entity_type.manager'); + $entity + = $type_manager + ->getStorage('entity_test') + ->create([]); + $value = 'This is an epic entity'; + $entity->field_fieldnote = $value; + $entity->name->value = $this->randomMachineName(); + $entity->save(); + + // Verify entity has been created properly. + $id = $entity->id(); + $entity + = $type_manager + ->getStorage('entity_test') + ->load($id); + + $this->assertTrue($entity->field_fieldnote instanceof FieldItemListInterface, 'Field implements interface.'); + $this->assertTrue($entity->field_fieldnote[0] instanceof FieldItemInterface, 'Field item implements interface.'); + $this->assertEqual($entity->field_fieldnote->value, $value); + $this->assertEqual($entity->field_fieldnote[0]->value, $value); + + // Verify changing the field's value. + $new_value = $this->randomMachineName(); + $entity->field_fieldnote->value = $new_value; + $this->assertEqual($entity->field_fieldnote->value, $new_value); + + // Read changed entity and assert changed values. + $entity->save(); + + $entity + = $type_manager + ->getStorage('entity_test') + ->load($id); + + $this->assertEqual($entity->field_fieldnote->value, $new_value); + + // Test sample item generation. + $entity + = $type_manager + ->getStorage('entity_test') + ->create([]); + + $entity->field_fieldnote->generateSampleItems(); + $this->entityValidateAndSave($entity); + } + + /** + * Test multiple access scenarios for the fieldnote field. + */ + public function testFieldNoteAccess() { + + // Let's set up some scenarios. + $scenarios = [ + 'admin_type' => [ + 'perms' => ['administer the fieldnote field'], + 'can_view_any' => TRUE, + 'can_edit_any' => TRUE, + 'can_view_own' => TRUE, + 'can_edit_own' => TRUE, + ], + 'low_access' => [ + 'perms' => ['view test entity'], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'view_any' => [ + 'perms' => [ + 'view test entity', + 'view any fieldnote', + ], + 'can_view_any' => TRUE, + 'can_edit_any' => FALSE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'edit_any' => [ + 'perms' => [ + 'view test entity', + 'view any fieldnote', + 'edit any fieldnote', + ], + 'can_view_any' => TRUE, + 'can_edit_any' => TRUE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'view_own' => [ + 'perms' => [ + 'view test entity', + 'view own fieldnote', + ], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => TRUE, + 'can_edit_own' => FALSE, + ], + 'edit_own' => [ + 'perms' => [ + 'view test entity', + 'view own fieldnote', + 'edit own fieldnote', + ], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => TRUE, + 'can_edit_own' => TRUE, + ], + ]; + + $value = 'This is an epic entity'; + // We also need to test users as an entity to attach to. They work + // a little differently than most content entity types: + $arbitrary_user = $this->createUser([], 'Some User'); + $arbitrary_user->user_fieldnote = $value; + $arbitrary_user->save(); + + foreach ($scenarios as $name => $scenario) { + $test_user = $this->createUser($scenario['perms'], $name); + $entity = entity_create('entity_test'); + $entity->field_fieldnote = $value; + $entity->name->value = $this->randomMachineName(); + $entity->save(); + + foreach (['can_view_any', 'can_edit_any'] as $op) { + $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]); + $this->doAccessAssertion($arbitrary_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]); + } + + if ($scenario['can_view_own'] or $scenario['can_edit_own']) { + $entity->user_id = $test_user; + $entity->save(); + $test_user->user_fieldnote = $value; + $test_user->save(); + + foreach (['can_view_own', 'can_edit_own'] as $op) { + $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]); + $this->doAccessAssertion($test_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]); + } + } + } + + } + + /** + * Helper routine to run the assertions. + */ + protected function doAccessAssertion($entity, $field_name, $account, $name, $op, $expected) { + $expect_str = $expected ? "CAN" : "CANNOT"; + $assert_str = "$name $expect_str do $op on field $field_name"; + $operation = preg_match('/edit/', $op) ? "edit" : "view"; + $result = $entity->$field_name->access($operation, $account); + if ($expected) { + $this->assertTrue($result, $assert_str); + } + else { + $this->assertFalse($result, $assert_str); + } + } + +} diff --git a/field_permission_example/templates/description.html.twig b/field_permission_example/templates/description.html.twig new file mode 100644 index 0000000..df673a1 --- /dev/null +++ b/field_permission_example/templates/description.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Contains the text of the field_permission_example explanation/description page + * + * Available variables: + * - admin_link: The translated link pointing to the administer permissions page. + */ +#} + +
+ +

The Field Permission Example module shows how you can restrict view and edit permissions + within your field implementation. It adds a new field type called Fieldnote. Fieldnotes + appear as simple text boxes on the create/edit form, and as sticky notes when viewed. + By 'sticky note' we mean 'Post-It Note' but that's a trademarked term.

+ +

To see this field in action, add it to a content type or user profile. Go to the + permissions page ({{admin_link}}) and look at the 'Field Permission Example' section. This + allows you to change which roles can see and edit Fieldnote fields.

+ +

Creating different users with different capabilities will let you see these behaviors + in action. Fieldnote helpfully displays a message telling you which permissions it is + trying to resolve for the current field/user combination.

+ +

Definitely look through the code to see various implementation details.

+ +