Commit fe09671b authored by Jonathan Sacksick's avatar Jonathan Sacksick
Browse files

fix: Add field level access control to licenses

parent 3db2b909
Loading
Loading
Loading
Loading
Loading
+5 −0
Changes for commerce_license.services.yml: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -63,3 +63,8 @@ services:
  commerce_license.cron:
    class: \Drupal\commerce_license\Cron
    arguments: ['@entity_type.manager', '@datetime.time']

  commerce_license.license_admin_route_subscriber:
    class: Drupal\commerce_license\EventSubscriber\LicenseAdminRouteSubscriber
    tags:
      - { name: event_subscriber }
+6 −1
Changes for src/Entity/License.php: 6 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 *   bundle_label = @Translation("License type"),
 *   bundle_plugin_type = "commerce_license_type",
 *   handlers = {
 *     "access" = "\Drupal\entity\UncacheableEntityAccessControlHandler",
 *     "access" = "\Drupal\commerce_license\LicenseAccessControlHandler",
 *     "event" = "Drupal\commerce_license\Event\LicenseEvent",
 *     "permission_provider" = "\Drupal\commerce_license\LicensePermissionProvider",
 *     "list_builder" = "Drupal\commerce_license\LicenseListBuilder",
@@ -74,6 +74,11 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 *     "state-transition-form" = "/admin/commerce/licenses/{commerce_license}/{field_name}/{transition_id}"
 *   },
 *   field_ui_base_route = "entity.commerce_license.field_ui_fields",
 *   restricted_fields = {
 *     "uid", "state", "product_variation", "expiration_type",
 *     "originating_order", "created", "changed", "granted", "renewed",
 *     "expires"
 *   },
 * )
 */
class License extends ContentEntityBase implements LicenseInterface {
+48 −0
Changes for src/EventSubscriber/LicenseAdminRouteSubscriber.php: 48 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\commerce_license\EventSubscriber;

use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\RouteCollection;

/**
 * Restricts the license admin routes to administrators.
 *
 * Customers can be granted "update own" and "delete own" access to their
 * licenses, so admin routes that only check license access would be
 * reachable by them. This applies to routes provided by state_machine and by
 * contrib modules, which is why the permission is enforced here.
 *
 * Routes that already require a permission are left untouched, to avoid
 * overriding or weakening it.
 *
 * @see \Drupal\state_machine\Routing\RouteSubscriber::alterRoutes()
 */
class LicenseAdminRouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection): void {
    foreach ($collection as $route) {
      $path = $route->getPath();
      if (!str_starts_with($path, '/admin/') ||
        !str_contains($path, '{commerce_license}') ||
        $route->hasRequirement('_permission')) {
        continue;
      }
      $route->setRequirement('_permission', 'access commerce administration pages');
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Run late, so that routes added by other subscribers are included.
    $events[RoutingEvents::ALTER] = ['onAlterRoutes', -1024];
    return $events;
  }

}
+46 −0
Changes for src/LicenseAccessControlHandler.php: 46 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\commerce_license;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity\UncacheableEntityAccessControlHandler;
use Drupal\field\FieldConfigInterface;

/**
 * Provides an access control handler for licenses.
 */
class LicenseAccessControlHandler extends UncacheableEntityAccessControlHandler {

  /**
   * {@inheritdoc}
   */
  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
    // Only users who can update any license through their permissions can
    // edit restricted fields. Customers can be granted "update own"
    // access, which must not allow changing the granted rights, the state,
    // the owner, etc. (e.g. through the edit form, JSON:API or REST).
    // Modules add their own fields to the "restricted_fields" entity
    // type property, in hook_entity_type_build().
    // Fields provided by the license type plugins hold the granted rights
    // (e.g. the role), so they are always restricted.
    $restricted_fields = $this->entityType->get('restricted_fields') ?? [];
    $is_bundle_plugin_field = !$field_definition->getFieldStorageDefinition()->isBaseField() && !$field_definition instanceof FieldConfigInterface;
    if ($operation == 'edit' && ($is_bundle_plugin_field || in_array($field_definition->getName(), $restricted_fields, TRUE))) {
      $permissions = [
        $this->entityType->getAdminPermission(),
        "update any {$this->entityTypeId}",
      ];
      // Users who can create licenses can set these fields on new licenses.
      if ($items && $items->getEntity()->isNew()) {
        $permissions[] = "create {$this->entityTypeId}";
      }
      return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
    }

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

}
+4 −0
Changes for src/LicensePermissionProvider.php: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -23,6 +23,10 @@ class LicensePermissionProvider extends UncacheableEntityPermissionProvider {

    // Mark the 'overview' permission as restricted.
    $permissions["access $entity_type_id overview"]['restrict access'] = TRUE;
    // Users who can create licenses or update any license can set the granted
    // rights (e.g. the role), so these permissions are restricted as well.
    $permissions["create $entity_type_id"]['restrict access'] = TRUE;
    $permissions["update any $entity_type_id"]['restrict access'] = TRUE;

    // Add a description to the 'create'  to make it clear that it only covers
    // admin creation, not creation via product purchase.
Loading