Fruits
Blogi

RESTful, Group and Nodeaccess - an efficient combination (part 3/4)

George Baev
George Baev

In the third part of our Decoupled System blog series, we describe the Drupal REST API functionality, PHP Traits that are used to check the user's permissions, how to create a compound response data structure and the Diagram CRUD operations.

Hi again! My name is George Baev and I am a Drupal developer at Sangre. This is the third part of a blog series describing the move from a Monolith to a Decoupled System in Drupal. In my previous post, I described the Drupal content types, their main fields and interconnections, and the data migration functionality of this project. The subject of this post is the Drupal REST API functionality which maintains the Diagrams. We'll see how the PHP Traits are used to check the user's permissions. We'll also gain an understanding of creating a compound response data structure. Finally, the Diagram CRUD operations will be described.

REST API

The Wikipedia Representational state transfer page describes the REST acronym as follows:

Representational State Transfer (REST) is an architectural style that defines a set of constraints to be used for creating web services. Web Services that conform to the REST architectural style, or RESTful web services, provide interoperability between computer systems on the Internet.

Resource Definitions

Each REST API resource is defined as a plugin resource, as shown in the example below:

namespace Drupal\my_module_diagrams\Plugin\resource;

/**
 * Class Diagrams.
 *
 * @package Drupal\my_module_diagrams\Plugin\resource
 *
 * @Resource(
 *   name = "diagrams:1.0",
 *   resource = "diagrams",
 *   label = "Diagrams",
 *   description = "A RESTful service resource exposing Diagrams.",
 *   authenticationTypes = TRUE,
 *   dataProvider = {
 *     "entityType": "node",
 *     "bundles": {
 *       "diagram"
 *     },
 *   },
 *   majorVersion = 1,
 *   minorVersion = 0
 * )
 */
class Diagrams__1_0 extends ResourceNode implements ResourceInterface {}

The most important part in this source code example is the Resource annotation. The resource attribute sets the trailing part in the endpoint's access URL, e.g. http://localhost/api/v1.0/diagrams . The authenticationTypes property could be set to boolean or an Array value. If set to TRUE, then all the authentication providers would be used until the user is authenticated. The dataProvider property defines that the resource would process diagram nodes. We will explain later, how it's possible to define a custom Data Provider for a resource.

Additional properties are defined on the Resource plugin definition properties page. These properties could be used for other specific settings related to the API endpoint.

Check Permissions PHP Traits

The Traits are described as follows:

Traits are a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies. The semantics of the combination of Traits and classes is defined in a way which reduces complexity, and avoids the typical problems associated with multiple inheritance and Mixins.

We use the Traits to authorize the user when working with the API. This is a trait which checks whether a user is permitted to execute a specified operation on a Diagram:

/**
  * Checks the Diagram entity access.
  *
  * @param integer $diagram_id
  *    The Diagram node id.
  * @param string $op
  *    Entity operation.
  * @return object
  *    The Diagram node object.
  */
protected function checkDiagramEntityAccess($diagram_id, $op = 'update', $account = NULL) {
  if (!(is_numeric($diagram_id) && $diagram_id > 0)) {
    throw new BadRequestException('Invalid Diagram Id parameter!');
  }

  $diagram = node_load($diagram_id);

  if (!is_object($diagram)) {
    throw new BadRequestException('The diagram does not exist!');
  }

  if (is_object($account)) {
    // Check if it's own diagram.
    if ($diagram->uid == $account->uid) {
      return $diagram;
    }

    // We must check the account's permissions for the diagram (nodeaccess).
    if ($this->diagramAccessible($op, $diagram, $account) == FALSE) {
      throw new ForbiddenException('The (' . $op . ') operation is not allowed for Diagram: ' . $diagram->nid);
    }
  }

  return $diagram;
}

/**
  * Checks the Diagram accessibility.
  *
  * @param string $op
  *    The diagram operation.
  * @param object $diagram
  *    The diagram object.
  * @param object $account
  *    The user account object.
  *
  * @return bool
  *    The result of the check.
  */
protected function diagramAccessible($op, $diagram, $account) {
  if (empty($diagram->group) || !is_numeric($diagram->group)) {
    // The diagram is not in a Group.
    return FALSE;
  }

  $group = group_load($diagram->group);

  if (!is_object($group)) {
    return FALSE;
  }

  if (isset($diagram->uid) && $diagram->uid == $account->uid) {
    // This is the Diagram owner.
    return TRUE;
  }

  $grants = _nodeaccess_get_grants($diagram);

  if (!empty($grants['uid'][$account->uid]['grant_' . $op])) {
    // The $op was explicitly granted to the account for this Diagram.
    return TRUE;
  }

  switch ($op) {
    case 'view':
      if ($group->userHasPermission($account->uid, 'can see all diagrams')) {
        return TRUE;
      }
      break;

    case 'create':
      if ($group->userHasPermission($account->uid, 'can create diagrams')) {
        return TRUE;
      }
      break;

    case 'update':
    case 'delete':
      if ($group->userHasPermission($account->uid, 'can update any diagram')) {
        return TRUE;
      }
      break;
  }

  return FALSE;
}

The predefined custom exceptions are part of the RESTful module. An error response is sent to the client when an exception is thrown. This allows us to use these custom exceptions to quickly break the code execution and inform the user about the problem. The diagramAccessible method compares the current node access and group user permissions with the required ones for the Diagram operation.

Compound Response Data Structures

We can include additional data in the API response by predefining some of the response's fields. This option reduces the number of API requests to the server. Here is an example:

/**
  * Overrides ResourceEntity::publicFieldsInfo().
  */
protected function publicFields() {
  $fields = parent::publicFields();

  $fields = array_merge($fields, $this->propFields());

  $fields['title'] = [
    'property' => 'title',
  ];

  $fields['owner_id'] = [
    'property' => 'author',
    'sub_property' => 'uid',
  ];

  $fields['markers'] = [
    'property' => 'field_diagram_markers',
    'callback' => [$this, 'Markers'],
  ];

  $fields['template_id'] = [
    'methods' => [],
  ];

  $fields['gid'] = [
    'methods' => [
      RequestInterface::METHOD_POST,
    ],
  ];

  return $fields;
}

The owner_id field includes the uid sub_property only. This way we exclude the username from the response.

The markers field contains a list of the Markers included in a Diagram. We execute the Markers method to get the data and the wrapped Diagram entity is passed as an argument to this method.

The template_id field is disabled in order to be used programmatically only.

The HTTP POST is the only method allowed for the gid field.

A complete guide about defining the public fields can be found on Defining the public fields page.

REST Data Providers

The endpoints definition allows setting a custom Data Provider class. This can be done as follows:

/**
  * {@inheritdoc}
  */
protected function dataProviderClassName() {
  return '\Drupal\my_module_diagrams\Plugin\resource\DataProviderDiagrams';
}

There are methods which allow dynamic setting of the Data Provider class. This is very useful when unit testing with stub classes.

The custom Data Provider class is defined as follows:

namespace Drupal\my_module_diagrams\Plugin\resource;

/**
 * Class DataProviderDiagrams.
 *
 * @package Drupal\my_module_diagrams\Plugin\resource
 */
class DataProviderDiagrams extends DataProviderNode {}

Please note the same namespace clause as the return value of the dataProviderClassName method.

CRUD Operations

The CRUD acronym is defined on Wikipedia as follows:

CRUD is an acronym for the four basic types of SQL commands: Create , Read , Update , Delete .

Most of the CRUD operations require an X-CSRF-Token. The RESTful Drupal module includes the resource to get the token:

curl -X GET \
  http://localhost/api/session/token \
  -H 'Authorization: Basic bearer-token' \
  -H 'Cache-Control: no-cache'

The CRUD operations related to the Diagram content type are listed below.

Create a Diagram

This is the HTTP POST request to create a Diagram:

curl -X POST \
  http://localhost/api/v1.0/diagrams \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: my-token' \
  -d '{
  "title": "Test Diagram",
  "language": "en",
  "template_id": 1,
  "gid": 1
}'

The request includes the template_id property, which is the Diagram Template node id to be used as the base for the new Diagram. The Diagram's Group ID is set also.

This POST request is processed by the following functionality:

/**
  * {@inheritdoc}
  */
public function create($object) {
  $this->validateBody($object);

  if ($this->checkEntityAccess('create', $this->entityType, $entity) === FALSE) {
    // User does not have access to create entity.
    throw new ForbiddenException('You do not have access to create a new resource.');
  }

  if (!empty($object['template_id']) && is_numeric($object['template_id'])) {
    // Create a Diagram from a template.
    $template = node_load($object['template_id']);
  }
  elseif (!empty($object['parent_diagram_id']) && is_numeric($object['parent_diagram_id'])) {
    // Clone a Diagram.
    $template = node_load($object['parent_diagram_id']);

    // Set the Diagram field values.
  }

  if (is_object($template)) {
    // Set the Paragraph fields.
  }
}

Retrieve a Diagram

This is the HTTP GET request to retrieve a Diagram:

curl -X GET \
  http://localhost/api/v1.0/diagrams/1 \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json'

The Diagram identifier is submitted as the last component of the URL path. This request is processed as follows:

/**
  * {@inheritdoc}
  */
public function view($identifier) {
  $this->checkDiagramEntityAccess($identifier, 'view', $this->getAccount());
  return parent::view($identifier);
}

The parent view method is overloaded by this method to check if the user is permitted to view the Diagram.

Update a Diagram

We use the following HTTP PATCH request to update a diagram:

curl -X PATCH \
  http://localhost/api/v1.0/diagrams/1 \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: my-token' \
  -d '{
  "title": "Diagram with updated title"
}'

The request includes both the Diagram identifier and the key-value data to update the Diagram properties. This is how the request is processed:

/**
  * {@inheritdoc}
  */
public function update($identifier, $object, $replace = FALSE) {
  $this->validateBody($object);
  $entity_id = $this->getEntityIdByFieldId($identifier);

  $diagram = $this->checkDiagramEntityAccess($entity_id, 'update', $this->getAccount());

  /* @var \EntityDrupalWrapper $wrapper */
  $wrapper = entity_metadata_wrapper($this->entityType, $diagram);

  $propFields = $this->propFields();

  $propFields['title'] = [
    'property' => 'title',
  ];

  foreach ($propFields as $key => $value) {
    if (isset($object[$key])) {
      $wrapper->{$value['property']}->set($object[$key]);
    }
  }

  $wrapper->save();

}

Delete a Diagram

The HTTP DELETE request is not complex:

curl -X DELETE \
  http://localhost/api/v1.0/diagrams/1 \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: my-token'

The user's permissions are checked before the Diagram has been deleted as seen in this source code example:

/**
  * {@inheritdoc}
  */
public function remove($identifier) {
  $diagram = $this->checkDiagramEntityAccess($identifier, 'delete');

  if ($diagram->uid == $this->getAccount()->uid) {
    // This is own Diagram.
    node_delete($diagram->nid);
  }
  else {
    if (!isset($diagram->group)) {
      throw new BadRequestException('The diagram does not belong to a group');
    }

    $group = $this->checkGroupPermission($diagram->group, 'can update any diagram');

    node_delete($diagram->nid);
  }

  // Set the HTTP headers.
  $this->setHttpHeader('Status', 204);
}

In my next post, I will describe the Ingestion, Search and Auth APIs. We'll gain an understanding of the details in the communication between Drupal and Elasticsearch. See you soon!

Jaa