1

I'm on D10.x.x and I've created a custom content entity - code below.

Those entities are created programmatically after an event, and one is assigned to each one of the site' users.

The users with authenticated role have the permission to edit their own entities; however they should be able to edit only a sub-set of the entity's fields.

I can't figure how to implement that access check.

From my example, the entity has like uid, status, created, etc.

How could I limit one role to edit only status or title field?

<?php

declare(strict_types=1);

namespace Drupal\my_module_notification\Entity;

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\my_module_notification\MyModuleNotificationUserInterface;
use Drupal\user\EntityOwnerTrait;

/**
 * Defines the my_module_notification_user entity class.
 *
 * @ContentEntityType(
 *   id = "my_module_notification_user",
 *   label = @Translation("User notification"),
 *   label_collection = @Translation("user notifications"),
 *   label_singular = @Translation("user notification"),
 *   label_plural = @Translation("user notifications"),
 *   label_count = @PluralTranslation(
 *     singular = "@count user notification",
 *     plural = "@count user notifications",
 *   ),
 *   handlers = {
 *     "list_builder" =
 *   "Drupal\my_module_notification\MyModuleNotificationUserListBuilder",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "access" =
 *   "Drupal\my_module_notification\MyModuleNotificationUserAccessControlHandler",
 *     "form" = {
 *       "add" = "Drupal\my_module_notification\Form\MyModuleNotificationUserForm",
 *       "edit" = "Drupal\my_module_notification\Form\MyModuleNotificationUserForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *       "delete-multiple-confirm" =
 *   "Drupal\Core\Entity\Form\DeleteMultipleForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   base_table = "my_module_notification_user",
 *   admin_permission = "administer my_module_notification_user",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "title",
 *     "uuid" = "uuid",
 *     "owner" = "uid",
 *   },
 *   links = {
 *     "collection" = "/admin/content/notification-user",
 *     "add-form" = "/notification-user/add",
 *     "canonical" = "/notification-user/{my_module_notification_user}",
 *     "edit-form" = "/notification-user/{my_module_notification_user}/edit",
 *     "delete-form" = "/notification-user/{my_module_notification_user}/delete",
 *     "delete-multiple-form" =
 *   "/admin/content/notification-user/delete-multiple",
 *   },
 *   field_ui_base_route = "entity.my_module_notification_user.edit_form",
 * )
 */
final class MyModuleNotificationUser extends ContentEntityBase implements MyModuleNotificationUserInterface {

  use EntityChangedTrait;
  use EntityOwnerTrait;

  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage): void {
    parent::preSave($storage);
    if (!$this->getOwnerId()) {
      // If no owner has been set explicitly, make the anonymous user the owner.
      $this->setOwnerId(0);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
    $fields = parent::baseFieldDefinitions($entity_type);
    $fields += self::ownerBaseFieldDefinitions($entity_type);

    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setRequired(TRUE)
      ->setSetting('max_length', 255)
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => -5,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'hidden',
        'type' => 'string',
        'weight' => -5,
      ])
      ->setDisplayConfigurable('view', TRUE);

    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Author'))
      ->setSetting('target_type', 'user')
      ->setDefaultValueCallback(self::class . '::getDefaultEntityOwner')
      ->setDisplayOptions('form', [
        'type' => 'entity_reference_autocomplete',
        'settings' => [
          'match_operator' => 'CONTAINS',
          'size' => 60,
          'placeholder' => '',
        ],
        'weight' => 1,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'author',
        'weight' => 1,
      ])
      ->setDisplayConfigurable('view', TRUE);

    $fields['my_module_notification'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Notification'))
      ->setRequired(TRUE)
      ->setCardinality(1)
      ->setSetting('target_type', 'my_module_notification')
      ->setDisplayOptions('form', [
        'type' => 'options_select',
        'weight' => 2,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'entity_reference_label',
        'weight' => 2,
      ])
      ->setDisplayConfigurable('view', TRUE);

    $fields['status'] = BaseFieldDefinition::create('list_string')
      ->setLabel(t('Status'))
      ->setSettings([
        'allowed_values' => [
          MyModuleNotificationUserInterface::UNREAD => t('Unread'),
          MyModuleNotificationUserInterface::READ => t('Read'),
          MyModuleNotificationUserInterface::ARCHIVED => t('Archived'),
        ],
      ])
      ->setDefaultValue('unread')
      ->setRequired(TRUE)
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'string',
        'weight' => 3,
      ])
      ->setDisplayOptions('form', [
        'type' => 'options_select',
        'weight' => 3,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Authored on'))
      ->setDescription(t('The time that the my_module_notification_user was created.'))
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'timestamp',
        'weight' => 10,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('form', [
        'type' => 'datetime_timestamp',
        'weight' => 10,
      ])
      ->setDisplayConfigurable('view', TRUE);

    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the my_module_notification_user was last edited.'));

    return $fields;
  }

  /**
   * {@inheritdoc}
   */
  public function getType(): string {
    return $this->bundle();
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle(): string {
    return $this->get('title')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setTitle($title): static {
    $this->set('title', $title);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCreatedTime(): int {
    return $this->get('created')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setCreatedTime($timestamp): static {
    $this->set('created', $timestamp);
    return $this;
  }

}

2 Answers 2

0

- Create an Access Control Handler.

  // src/MyModuleNotificationUserAccessControlHandler.php

namespace Drupal\my_module_notification;

use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Defines the access control handler for the MyModuleNotificationUser entity type.
 */
class MyModuleNotificationUserAccessControlHandler extends EntityAccessControlHandler {
  
  /**
   * {@inheritdoc}
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    // Check the operation being performed.
    switch ($operation) {
      case 'update':
        // Check if the user has the 'edit title' or 'edit status' permission.
        if (
          ($account->hasPermission('edit title') && $entity->access('update', $account, TRUE)) ||
          ($account->hasPermission('edit status') && $entity->access('update', $account, TRUE))
        ) {
          return AccessResult::allowed();
        }
        return AccessResult::forbidden();

      // Add additional cases for other operations if needed.

      default:
        // For other operations, fall back to the default checkAccess method.
        return parent::checkAccess($entity, $operation, $account);
    }
  }
}

- Define Permissions my_module_notification.permissions.yml

- Then use the Access Control Handler in my_module_notification.module

/**
 * Implements hook_entity_type_alter().
 */
function my_module_notification_entity_type_alter(array &$entity_types) {
  // Update the access control handler for MyModuleNotificationUser entities.
  $entity_types['my_module_notification_user']->setHandlerClass('access', MyModuleNotificationUserAccessControlHandler::class);
}
3
  • Thanks for the answer, but I think this code isn't a valid solution: the hook_entity_access run at entity level. Therefore, if I test it with an "authenticated" user, he is able to edit the entity, because the $account has edit access at least one $restricted_fields. However, the user is still able to edit all the fields on the entity. By the way, hook_entity_access requires to return a AccessResultInterface object, not a bool.
    – Giuseppe
    Dec 5, 2023 at 8:40
  • Create file in src/MyModuleNotificationUserAccessControlHandler.php then add checkaccess method.
    – Barry
    Dec 6, 2023 at 0:02
  • checkAccess() is for the operations on the entity itself, not for operations on the entity fields. hook_entity_type_alter() is not necessary, since the OP implements a custom entity. It is sufficient to define the access control handler in the entity annotation, which is what the shown code already does.
    – apaderno
    Dec 17, 2023 at 14:09
0

Since the entity defined from your module has an access handler (Drupal\my_module_notification\MyModuleNotificationUserAccessControlHandler), it is sufficient to implement checkFieldAccess() in that class, similarly to what the NodeAccessControlHandler class does with its checkFieldAccess() method. Do not confuse it with the checkAccess() and access() methods, which are called for operations on the entity itself, not on the entity fields.

  // Only users with the administer nodes permission can edit administrative
  // fields.
  $administrative_fields = [
    'uid',
    'status',
    'created',
    'promote',
    'sticky',
  ];
  if ($operation == 'edit' && in_array($field_definition->getName(), $administrative_fields, TRUE)) {
    return AccessResult::allowedIfHasPermission($account, 'administer nodes');
  }

  // No user can change read only fields.
  $read_only_fields = [
    'revision_timestamp',
    'revision_uid',
  ];
  if ($operation == 'edit' && in_array($field_definition->getName(), $read_only_fields, TRUE)) {
    return AccessResult::forbidden();
  }

  // Users have access to the revision_log field either if they have
  // administrative permissions or if the new revision option is enabled.
  if ($operation == 'edit' && $field_definition->getName() == 'revision_log') {
    if ($account->hasPermission('administer nodes')) {
      return AccessResult::allowed()->cachePerPermissions();
    }
    return AccessResult::allowedIf($items->getEntity()->type->entity->shouldCreateNewRevision())->cachePerPermissions();
  }
  return parent::checkFieldAccess($operation, $field_definition, $account, $items);

That method is called by the fieldAccess() method implemented by the EntityAccessControlHandler class, which usually is the parent class used by access control handlers.

  $account = $this->prepareUser($account);

  // Get the default access restriction that lives within this field.
  $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();

  // Explicitly disallow changing the entity ID and entity UUID.
  $entity = $items ? $items->getEntity() : NULL;
  if ($operation === 'edit' && $entity) {
    if ($field_definition->getName() === $this->entityType->getKey('id')) {
      // String IDs can be set when creating the entity.
      if (!($entity->isNew() && $field_definition->getType() === 'string')) {
        return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE;
      }
    }
    elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) { 
      // UUIDs can be set when creating an entity.
      if (!$entity->isNew()) {
        return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE;
      }
    }
  }

  // Get the default access restriction as specified by the access control
  // handler.
  $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);

  // Combine default access, denying access wins.
  $default = $default->andIf($entity_default);

  // Invoke hook and collect grants/denies for field access from other
  // modules.
  $grants = [];
  $this->moduleHandler()->invokeAllWith('entity_field_access', function (callable $hook, string $module) use ($operation, $field_definition, $account, $items, &$grants) {
    $grants[] = [
      $module => $hook($operation, $field_definition, $account, $items),
    ];
  });

  // Our default access flag is masked under the ':default' key.
  $grants = array_merge([':default' => $default], ...$grants);

  // Also allow modules to alter the returned grants/denies.
  $context = [
    'operation' => $operation,
    'field_definition' => $field_definition,
    'items' => $items,
    'account' => $account,
  ];
  $this->moduleHandler()->alter('entity_field_access', $grants, $context);
  $result = $this->processAccessHookResults($grants);
  return $return_as_object ? $result : $result->isAllowed();

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.