Commit 085d7b21 authored by Jonathan Sacksick's avatar Jonathan Sacksick
Browse files

fix: Add field level access control to shipments

parent fdc73fba
Loading
Loading
Loading
Loading
Loading
+5 −0
Changes for src/Entity/Shipment.php: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -81,6 +81,11 @@ use Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface;
 *   },
 *   bundle_entity_type = "commerce_shipment_type",
 *   field_ui_base_route = "entity.commerce_shipment_type.edit_form",
 *   restricted_fields = {
 *     "order_id", "package_type", "shipping_service", "service_label",
 *     "title", "items", "weight", "original_amount", "amount", "adjustments",
 *     "tracking_code", "state", "data", "shipped",
 *   },
 * )
 */
class Shipment extends ContentEntityBase implements ShipmentInterface {
+17 −0
Changes for src/Hook/CommerceShippingHooks.php: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -244,6 +244,23 @@ class CommerceShippingHooks {
   */
  #[Hook('entity_field_access')]
  public function entityFieldAccess(string $operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
    // Only users who can update orders through their permissions can change
    // which shipments an order references. Customers can be granted "update"
    // access to their own cart (by commerce_cart), which must not allow them
    // to reference other shipments.
    if ($operation === 'edit' && $field_definition->getTargetEntityTypeId() === 'commerce_order' && $field_definition->getName() === 'shipments') {
      $permissions = ['administer commerce_order'];
      $order = $items?->getEntity();
      $order_type_id = $order ? $order->bundle() : $field_definition->getTargetBundle();
      if ($order_type_id) {
        $permissions[] = "update $order_type_id commerce_order";
        // Allow setting the shipments of new orders (e.g. in the admin UI).
        if ($order && $order->isNew()) {
          $permissions[] = "create $order_type_id commerce_order";
        }
      }
      return AccessResult::forbiddenIf(!array_filter($permissions, [$account, 'hasPermission']))->cachePerPermissions();
    }
    if (!$this->moduleHandler->moduleExists('jsonapi')) {
      return AccessResult::neutral();
    }
+36 −0
Changes for src/ShipmentAccessControlHandler.php: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@ namespace Drupal\commerce_shipping;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\commerce_shipping\Entity\ShipmentInterface;

@@ -19,6 +21,8 @@ use Drupal\commerce_shipping\Entity\ShipmentInterface;
 *   parent order can be resent.
 * - An shipment can be created, updated or deleted if the user has the
 *   "manage $bundle shipments" permission.
 * - Restricted fields (state, items, amounts, etc.) can only be edited
 *   with the "manage $bundle shipments" permission.
 *
 * The "administer commerce_shipment" permission is also respected.
 */
@@ -51,6 +55,38 @@ class ShipmentAccessControlHandler extends CoreEntityAccessControlHandler {
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
    // Only users who can manage shipments or update orders through their
    // permissions can edit restricted fields. Shipment "update" access also
    // follows the order, which is granted to customers for their own cart (by
    // commerce_cart), and must not allow changing the state, items, amounts,
    // etc. (e.g. through JSON:API or REST). The checkout form only uses the
    // shipping method and the shipping profile.
    // Modules add their own fields to the "restricted_fields" entity
    // type property, in hook_entity_type_build().
    $restricted_fields = $this->entityType->get('restricted_fields') ?? [];
    if ($operation == 'edit' && in_array($field_definition->getName(), $restricted_fields, TRUE)) {
      $permissions = [$this->entityType->getAdminPermission(), 'administer commerce_order'];
      /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface|null $shipment */
      $shipment = $items?->getEntity();
      $bundle = $shipment ? $shipment->bundle() : $field_definition->getTargetBundle();
      if ($bundle) {
        $permissions[] = "manage $bundle commerce_shipment";
      }
      // Staff who can update the order through their permissions can edit its
      // shipments (e.g. the tracking code), like the order's "shipments" field.
      if ($order_type_id = $shipment?->getOrder()?->bundle()) {
        $permissions[] = "update $order_type_id commerce_order";
      }
      return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
    }

    return parent::checkFieldAccess($operation, $field_definition, $account, $items);
  }

  /**
   * {@inheritdoc}
   */
+6 −0
Changes for tests/modules/commerce_shipping_field_access_test/commerce_shipping_field_access_test.info.yml: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
name: Commerce Shipping Field Access Test
type: module
description: Adds a restricted shipment field, as other modules do.
package: Testing
dependencies:
  - commerce_shipping:commerce_shipping
+44 −0
Changes for tests/modules/commerce_shipping_field_access_test/commerce_shipping_field_access_test.module: 44 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Adds a restricted shipment field, as other modules do.
 */

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Implements hook_entity_base_field_info().
 */
function commerce_shipping_field_access_test_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type->id() === 'commerce_shipment') {
    $fields['test_restricted_field'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Test restricted field'));
  }
  return $fields;
}

/**
 * Implements hook_entity_type_build().
 */
function commerce_shipping_field_access_test_entity_type_build(array &$entity_types) {
  $restricted_fields = $entity_types['commerce_shipment']->get('restricted_fields') ?? [];
  $entity_types['commerce_shipment']->set('restricted_fields', array_merge($restricted_fields, ['test_restricted_field']));
}

/**
 * Implements hook_entity_field_access().
 */
function commerce_shipping_field_access_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
  if ($operation === 'edit' && $field_definition->getName() === 'test_restricted_field') {
    // Modules can grant access to restricted fields.
    return AccessResult::allowedIfHasPermission($account, 'edit test restricted field');
  }
  return AccessResult::neutral();
}
Loading