summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/jsonapi/jsonapi.api.php
blob: 5b2f2002d25c12aad76df140717834d7b7208500 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
<?php

/**
 * @file
 * Documentation related to JSON:API.
 */

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

/**
 * @defgroup jsonapi_architecture JSON:API Architecture
 * @{
 *
 * @section overview Overview
 * The JSON:API module is a Drupal-centric implementation of the JSON:API
 * specification. By its own definition, the JSON:API specification "is a
 * specification for how a client should request that resources be fetched or
 * modified, and how a server should respond to those requests. [It] is designed
 * to minimize both the number of requests and the amount of data transmitted
 * between clients and servers. This efficiency is achieved without compromising
 * readability, flexibility, or discoverability."
 *
 * While "Drupal-centric", the JSON:API module is committed to strict compliance
 * with the specification. Wherever possible, the module attempts to implement
 * the specification in a way which is compatible and familiar with the patterns
 * and concepts inherent to Drupal. However, when "Drupalisms" cannot be
 * reconciled with the specification, the module will always choose the
 * implementation most faithful to the specification.
 *
 * @see http://jsonapi.org/
 *
 * @section resources Resources
 * Every unit of data in the specification is a "resource". The specification
 * defines how a client should interact with a server to fetch and manipulate
 * these resources.
 *
 * The JSON:API module maps every entity type + bundle to a resource type.
 * Since the specification does not have a concept of resource type inheritance
 * or composition, the JSON:API module implements different bundles of the same
 * entity type as *distinct* resource types.
 *
 * While it is theoretically possible to expose arbitrary data as resources, the
 * JSON:API module only exposes resources from (config and content) entities.
 * This eliminates the need for another abstraction layer in order implement
 * certain features of the specification.
 *
 * @section relationships Relationships
 * The specification defines semantics for the "relationships" between
 * resources. Since the JSON:API module defines every entity type + bundle as a
 * resource type and does not allow non-entity resources, it is able to use
 * entity references to automatically define and represent the relationships
 * between all resources.
 *
 * @section revisions Resource versioning
 * The JSON:API module exposes entity revisions in a manner inspired by RFC5829:
 * Link Relation Types for Simple Version Navigation between Web Resources.
 *
 * Revision support is not an official part of the JSON:API specification.
 * However, a number of "profiles" are being developed (also not officially part
 * in the spec, but already committed to JSON:API v1.1) to standardize any
 * custom behaviors that the JSON:API module has developed (all of which are
 * still specification-compliant).
 *
 * @see https://github.com/json-api/json-api/pull/1268
 * @see https://github.com/json-api/json-api/pull/1311
 * @see https://www.drupal.org/project/drupal/issues/2955020
 *
 * By implementing revision support as a profile, the JSON:API module should be
 * maximally compatible with other systems.
 *
 * A "version" in the JSON:API module is any revision that was previously, or is
 * currently, a default revision. Not all revisions are considered to be a
 * "version". Revisions that are not marked as a "default" revision are
 * considered "working copies" since they are not usually publicly available
 * and are the revisions to which most new work is applied.
 *
 * When the Content Moderation module is installed, it is possible that the
 * most recent default revision is *not* the latest revision.
 *
 * Requesting a resource version is done via a URL query parameter. It has the
 * following form:
 *
 * @code
 *              version-identifier
 *                    __|__
 *                   /     \
 * ?resourceVersion=foo:bar
 *                   \_/ \_/
 *                    |   |
 *    version-negotiator  |
 *                version-argument
 * @endcode
 *
 * A version identifier is a string with enough information to load a
 * particular revision. The version negotiator component names the negotiation
 * mechanism for loading a revision. Currently, this can be either `id` or
 * `rel`. The `id` negotiator takes a version argument which is the desired
 * revision ID. The `rel` negotiator takes a version argument which is either
 * the string `latest-version` or the string `working-copy`.
 *
 * In the future, other negotiators may be developed, such as negotiators that
 * are UUID-, timestamp-, or workspace-based.
 *
 * To illustrate how a particular entity revision is requested, imagine a node
 * that has a "Published" revision and a subsequent "Draft" revision.
 *
 * Using JSON:API, one could request the "Published" node by requesting
 * `/jsonapi/node/page/{{uuid}}?resourceVersion=rel:latest-version`.
 *
 * To preview an entity that is still a work-in-progress (i.e. the "Draft"
 * revision) one could request
 * `/jsonapi/node/page/{{uuid}}?resourceVersion=rel:working-copy`.
 *
 * To request a specific revision ID, one can request
 * `/jsonapi/node/page/{{uuid}}?resourceVersion=id:{{revision_id}}`.
 *
 * It is not yet possible to request a collection of revisions. This is still
 * under development in issue [#3009588].
 *
 * @see https://www.drupal.org/project/drupal/issues/3009588.
 * @see https://tools.ietf.org/html/rfc5829
 * @see https://www.drupal.org/docs/8/modules/jsonapi/revisions
 *
 * @section translations Resource translations
 *
 * Some multilingual features currently do not work well with JSON:API. See
 * JSON:API modules' multilingual support documentation online for more
 * information on the current status of multilingual support.
 *
 * @see https://www.drupal.org/docs/8/modules/jsonapi/translations
 *
 * @section api API
 * The JSON:API module provides an HTTP API that adheres to the JSON:API
 * specification.
 *
 * The JSON:API module provides *no PHP API to modify its behavior.* It is
 * designed to have zero configuration.
 *
 * - Adding new resources/resource types is unsupported: all entities/entity
 *   types are exposed automatically. If you want to expose more data via the
 *   JSON:API module, the data must be defined as entity. See the "Resources"
 *   section.
 * - Custom field type normalization is not supported because the JSON:API
 *   specification requires specific representations for resources (entities),
 *   attributes on resources (non-entity reference fields) and relationships
 *   between those resources (entity reference fields). A field contains
 *   properties, and properties are of a certain data type. All non-internal
 *   properties on a field are normalized.
 * - The same data type normalizers as those used by core's Serialization and
 *   REST modules are also used by the JSON:API module.
 * - All available authentication mechanisms are allowed.
 *
 * @section tests Test Coverage
 * The JSON:API module comes with extensive unit and kernel tests. But most
 * importantly for end users, it also has comprehensive integration tests. These
 * integration tests are designed to:
 *
 * - ensure a great DX (Developer Experience)
 * - detect regressions and normalization changes before shipping a release
 * - guarantee 100% of Drupal core's entity types work as expected
 *
 * The integration tests test the same common cases and edge cases using
 * \Drupal\Tests\jsonapi\Functional\ResourceTestBase, which is a base class
 * subclassed for every entity type that Drupal core ships with. It is ensured
 * that 100% of Drupal core's entity types are tested thanks to
 * \Drupal\Tests\jsonapi\Functional\TestCoverageTest.
 *
 * Custom entity type developers can get the same assurances by subclassing it
 * for their entity types.
 *
 * @section bc Backwards Compatibility
 * PHP API: there is no PHP API except for three security-related hooks. This
 * means that this module's implementation details are entirely free to
 * change at any time.
 *
 * Note that *normalizers are internal implementation details.* While
 * normalizers are services, they are *not* to be used directly. This is due to
 * the design of the Symfony Serialization component, not because the JSON:API
 * module wanted to publicly expose services.
 *
 * HTTP API: URLs and JSON response structures are considered part of this
 * module's public API. However, inconsistencies with the JSON:API specification
 * will be considered bugs. Fixes which bring the module into compliance with
 * the specification are *not* guaranteed to be backwards-compatible. When
 * compliance bugs are found, clients are expected to be made compatible with
 * both the pre-fix and post-fix representations.
 *
 * What this means for developing consumers of the HTTP API is that *clients
 * should be implemented from the specification first and foremost.* This should
 * mitigate implicit dependencies on implementation details or inconsistencies
 * with the specification that are specific to this module.
 *
 * To help develop compatible clients, every response indicates the version of
 * the JSON:API specification used under its "jsonapi" key. Future releases
 * *may* increment the minor version number if the module implements features of
 * a later specification. Remember that the specification stipulates that future
 * versions *will* remain backwards-compatible as only additions may be
 * released.
 *
 * @see http://jsonapi.org/faq/#what-is-the-meaning-of-json-apis-version
 *
 * Tests: subclasses of base test classes may contain BC breaks between minor
 * releases, to allow minor releases to A) comply better with the JSON:API spec,
 * B) guarantee that all resource types (and therefore entity types) function as
 * expected, C) update to future versions of the JSON:API spec.
 *
 * @}
 */

/**
 * @addtogroup hooks
 * @{
 */

/**
 * Controls access when filtering by entity data via JSON:API.
 *
 * This module supports filtering by resource object attributes referenced by
 * relationship fields. For example, a site may add a "Favorite Animal" field
 * to user entities, which would permit the following filtered query:
 * @code
 * /jsonapi/node/article?filter[uid.field_favorite_animal]=llama
 * @endcode
 * This query would return articles authored by users whose favorite animal is a
 * llama. However, the information about a user's favorite animal should not be
 * available to users without the "access user profiles" permission. The same
 * must hold true even if that user is referenced as an article's author.
 * Therefore, access to filter by this data must be restricted so that access
 * cannot be bypassed via a JSON:API filtered query.
 *
 * As a rule, clients should only be able to filter by data that they can
 * view.
 *
 * Conventionally, `$entity->access('view')` is how entity access is checked.
 * This call invokes the corresponding hooks. However, these access checks
 * require an `$entity` object. This means that they cannot be called prior to
 * executing a database query.
 *
 * In order to safely enable filtering across a relationship, modules
 * responsible for entity access must do two things:
 * - Implement this hook (or hook_jsonapi_ENTITY_TYPE_filter_access()) and
 *   return an array of AccessResults keyed by the named entity subsets below.
 * - If the AccessResult::allowed() returned by the above hook does not provide
 *   enough granularity (for example, if access depends on a bundle field value
 *   of the entity being queried), then hook_query_TAG_alter() must be
 *   implemented using the 'entity_access' or 'ENTITY_TYPE_access' query tag.
 *   See node_query_node_access_alter() for an example.
 *
 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
 *   The entity type of the entity to be filtered upon.
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The account for which to check access.
 *
 * @return \Drupal\Core\Access\AccessResultInterface[]
 *   An array keyed by a constant which identifies a subset of entities. For
 *   each subset, the value is one of the following access results:
 *   - AccessResult::allowed() if all entities within the subset (potentially
 *     narrowed by hook_query_TAG_alter() implementations) are viewable.
 *   - AccessResult::forbidden() if any entity within the subset is not
 *     viewable.
 *   - AccessResult::neutral() if the implementation has no opinion.
 *   The supported subsets for which an access result may be returned are:
 *   - JSONAPI_FILTER_AMONG_ALL: all entities of the given type.
 *   - JSONAPI_FILTER_AMONG_PUBLISHED: all published entities of the given type.
 *   - JSONAPI_FILTER_AMONG_ENABLED: all enabled entities of the given type.
 *   - JSONAPI_FILTER_AMONG_OWN: all entities of the given type owned by the
 *     user for whom access is being checked.
 *   See the documentation of the above constants for more information about
 *   each subset.
 *
 * @see hook_jsonapi_ENTITY_TYPE_filter_access()
 */
function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  // For every entity type that has an admin permission, allow access to filter
  // by all entities of that type to users with that permission.
  if ($admin_permission = $entity_type->getAdminPermission()) {
    return ([
      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
    ]);
  }
}

/**
 * Controls access to filtering by entity data via JSON:API.
 *
 * This is the entity-type-specific variant of
 * hook_jsonapi_entity_filter_access(). For implementations with logic that is
 * specific to a single entity type, it is recommended to implement this hook
 * rather than the generic hook_jsonapi_entity_filter_access() hook, which is
 * called for every entity type.
 *
 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
 *   The entity type of the entities to be filtered upon.
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The account for which to check access.
 *
 * @return \Drupal\Core\Access\AccessResultInterface[]
 *   The array of access results, keyed by subset. See
 *   hook_jsonapi_entity_filter_access() for details.
 *
 * @see hook_jsonapi_entity_filter_access()
 */
function hook_jsonapi_ENTITY_TYPE_filter_access(EntityTypeInterface $entity_type, AccountInterface $account): array {
  return ([
    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'),
    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'),
    JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'),
  ]);
}

/**
 * Restricts filtering access to the given field.
 *
 * Some fields may contain sensitive information. In these cases, modules are
 * supposed to implement hook_entity_field_access(). However, this hook receives
 * an optional `$items` argument and often must return AccessResult::neutral()
 * when `$items === NULL`. This is because access may or may not be allowed
 * based on the field items or based on the entity on which the field is
 * attached (if the user is the entity owner, for example).
 *
 * Since JSON:API must check field access prior to having a field item list
 * instance available (access must be checked before a database query is made),
 * it is not sufficiently secure to check field 'view' access alone.
 *
 * This hook exists so that modules which cannot return
 * AccessResult::forbidden() from hook_entity_field_access() can still secure
 * JSON:API requests where necessary.
 *
 * If a corresponding implementation of hook_entity_field_access() *can* be
 * forbidden for one or more values of the `$items` argument, this hook *MUST*
 * return AccessResult::forbidden().
 *
 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
 *   The field definition of the field to be filtered upon.
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The account for which to check access.
 *
 * @return \Drupal\Core\Access\AccessResultInterface
 *   The access result.
 */
function hook_jsonapi_entity_field_filter_access(FieldDefinitionInterface $field_definition, AccountInterface $account) {
  if ($field_definition->getTargetEntityTypeId() === 'node' && $field_definition->getName() === 'field_sensitive_data') {
    $has_sufficient_access = FALSE;
    foreach (['administer nodes', 'view all sensitive field data'] as $permission) {
      $has_sufficient_access = $has_sufficient_access ?: $account->hasPermission($permission);
    }
    return AccessResult::forbiddenIf(!$has_sufficient_access)->cachePerPermissions();
  }
  return AccessResult::neutral();
}

/**
 * @} End of "addtogroup hooks".
 */