diff --git a/core/modules/jsonapi/jsonapi.module b/core/modules/jsonapi/jsonapi.module index d0a58ecf9c..f70fc481bc 100644 --- a/core/modules/jsonapi/jsonapi.module +++ b/core/modules/jsonapi/jsonapi.module @@ -319,7 +319,7 @@ function jsonapi_jsonapi_user_filter_access(EntityTypeInterface $entity_type, Ac /** * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'workspace'. */ -function jsonapi_jsonapi_workspace_filter_access(EntityTypeInterface $entity_type, $published, $owner, AccountInterface $account) { +function jsonapi_jsonapi_workspace_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) { // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess() return ([ JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'), diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index 3c39f097e8..e98c5042f0 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -920,7 +920,9 @@ public function testGetIndividual() { $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability(); $reason = $this->getExpectedUnauthorizedAccessMessage('GET'); $message = trim("The current user is not allowed to GET the selected resource. $reason"); - $this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), FALSE, 'MISS'); + // MISS or UNCACHEABLE depends on data. It must not be HIT. + $dynamic_cache_header_value = !empty(array_intersect(['user', 'session'], $expected_403_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), FALSE, $dynamic_cache_header_value); $this->assertArrayNotHasKey('Link', $response->getHeaders()); } else { @@ -1076,7 +1078,7 @@ public function testCollection() { $expected_cacheability = $expected_response->getCacheableMetadata(); $response = $this->request('HEAD', $collection_url, $request_options); // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); // Different databases have different sort orders, so a sort is required so @@ -1089,6 +1091,8 @@ public function testCollection() { // self::getExpectedCollectionResponse(). $expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options); $expected_cacheability = $expected_response->getCacheableMetadata(); + // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $collection_url, $request_options); $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); @@ -1098,6 +1102,8 @@ public function testCollection() { // 200 for well-formed HEAD request. $expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options); $expected_cacheability = $expected_response->getCacheableMetadata(); + // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; $response = $this->request('HEAD', $collection_url, $request_options); $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); @@ -1374,7 +1380,7 @@ protected function doTestRelated(array $request_options) { FALSE, $actual_response->getStatusCode() === 200 ? ($expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS') - : FALSE + : (!empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : FALSE) ); } } @@ -1409,7 +1415,9 @@ protected function doTestRelationshipGet(array $request_options) { $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, - $expected_resource_response->isSuccessful() ? 'MISS' : FALSE + empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) + ? $expected_resource_response->isSuccessful() ? 'MISS' : FALSE + : 'UNCACHEABLE' ); } } diff --git a/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php b/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php new file mode 100644 index 0000000000..32bcb7d799 --- /dev/null +++ b/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php @@ -0,0 +1,223 @@ + NULL, + ]; + + /** + * {@inheritdoc} + */ + protected static $uniqueFieldNames = ['id']; + + /** + * {@inheritdoc} + */ + protected static $firstCreatedEntityId = 'updated_campaign'; + + /** + * {@inheritdoc} + */ + protected static $secondCreatedEntityId = 'updated_campaign'; + + /** + * {@inheritdoc} + * + * @var \Drupal\workspaces\WorkspaceInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected function setUpAuthorization($method) { + switch ($method) { + case 'GET': + $this->grantPermissionsToTestedRole(['view any workspace']); + break; + + case 'POST': + $this->grantPermissionsToTestedRole(['create workspace']); + break; + + case 'PATCH': + $this->grantPermissionsToTestedRole(['edit any workspace']); + break; + + case 'DELETE': + $this->grantPermissionsToTestedRole(['delete any workspace']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity = Workspace::create([ + 'id' => 'campaign', + 'label' => 'Campaign', + 'uid' => $this->account->id(), + 'created' => 123456789, + ]); + $entity->save(); + return $entity; + } + + /** + * {@inheritdoc} + */ + protected function createAnotherEntity($key) { + $duplicate = $this->getEntityDuplicate($this->entity, $key); + $duplicate->set('id', 'duplicate'); + $duplicate->set('field_rest_test', 'Second collection entity'); + $duplicate->save(); + return $duplicate; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedDocument() { + $author = User::load($this->entity->getOwnerId()); + $base_url = Url::fromUri('base:/jsonapi/workspace/workspace/' . $this->entity->uuid())->setAbsolute(); + return [ + 'jsonapi' => [ + 'meta' => [ + 'links' => [ + 'self' => ['href' => 'http://jsonapi.org/format/1.0/'], + ], + ], + 'version' => '1.0', + ], + 'links' => [ + 'self' => ['href' => $base_url->toString()], + ], + 'data' => [ + 'id' => $this->entity->uuid(), + 'type' => static::$resourceTypeName, + 'links' => [ + 'self' => ['href' => $base_url->toString()], + ], + 'attributes' => [ + 'created' => '1973-11-29T21:33:09+00:00', + 'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339), + 'label' => 'Campaign', + 'drupal_internal__id' => 'campaign', + 'drupal_internal__revision_id' => 2, + ], + 'relationships' => [ + 'uid' => [ + 'data' => [ + 'id' => $author->uuid(), + 'type' => 'user--user', + ], + 'links' => [ + 'related' => [ + 'href' => $base_url->toString() . '/uid', + ], + 'self' => [ + 'href' => $base_url->toString() . '/relationships/uid', + ], + ], + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getPostDocument() { + return [ + 'data' => [ + 'type' => static::$resourceTypeName, + 'attributes' => [ + 'drupal_internal__id' => 'updated_campaign', + 'label' => 'Updated campaign', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getPatchDocument() { + $patch_document = parent::getPatchDocument(); + unset($patch_document['data']['attributes']['drupal_internal__id']); + return $patch_document; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessCacheability() { + // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess() + return parent::getExpectedUnauthorizedAccessCacheability() + ->addCacheTags(['workspace:campaign']) + // The "view|edit|delete own workspace" permissions add the 'user' cache + // context. + ->addCacheContexts(['user']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + switch ($method) { + case 'GET': + return "The 'view own workspace' permission is required."; + case 'POST': + return "The following permissions are required: 'administer workspaces' OR 'create workspace'."; + case 'PATCH': + return "The 'edit own workspace' permission is required."; + case 'DELETE': + return "The 'delete own workspace' permission is required."; + } + } + + /** + * {@inheritdoc} + */ + protected function getSparseFieldSets() { + // Workspace's resource type name ('workspace') comes after the 'uid' field, + // which breaks nested sparse fieldset tests. + return array_diff_key(parent::getSparseFieldSets(), array_flip([ + 'nested_empty_fieldset', + 'nested_fieldset_with_owner_fieldset', + ])); + } + +} diff --git a/core/modules/workspaces/src/EntityTypeInfo.php b/core/modules/workspaces/src/EntityTypeInfo.php index 4f0ba1a7d4..40b457453a 100644 --- a/core/modules/workspaces/src/EntityTypeInfo.php +++ b/core/modules/workspaces/src/EntityTypeInfo.php @@ -86,6 +86,11 @@ public function entityTypeBuild(array &$entity_types) { * @see hook_entity_type_alter() */ public function entityTypeAlter(array &$entity_types) { + // Workspace entities can not be moderated because they use string IDs. + // @see \Drupal\content_moderation\Entity\ContentModerationState::baseFieldDefinitions() + // where the target entity ID is defined as an integer. + $entity_types['workspace']->setHandlerClass('moderation', ''); + foreach ($entity_types as $entity_type_id => $entity_type) { // Non-default workspaces display the active revision on the canonical // route of an entity, so the latest version route is no longer needed. diff --git a/core/modules/workspaces/src/WorkspaceAccessControlHandler.php b/core/modules/workspaces/src/WorkspaceAccessControlHandler.php index ae107f3950..ffa0e9b5a0 100644 --- a/core/modules/workspaces/src/WorkspaceAccessControlHandler.php +++ b/core/modules/workspaces/src/WorkspaceAccessControlHandler.php @@ -45,7 +45,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'create workspace'); + return AccessResult::allowedIfHasPermissions($account, ['administer workspaces', 'create workspace'], 'OR'); } } diff --git a/core/modules/workspaces/tests/src/Functional/Hal/WorkspaceHalJsonAnonTest.php b/core/modules/workspaces/tests/src/Functional/Hal/WorkspaceHalJsonAnonTest.php new file mode 100644 index 0000000000..3e5076d333 --- /dev/null +++ b/core/modules/workspaces/tests/src/Functional/Hal/WorkspaceHalJsonAnonTest.php @@ -0,0 +1,29 @@ +applyHalFieldNormalization($default_normalization); + $author = User::load($this->entity->getOwnerId()); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => '', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/workspace/workspace', + ], + $this->baseUrl . '/rest/relation/workspace/workspace/uid' => [ + [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + ], + ], + '_embedded' => [ + $this->baseUrl . '/rest/relation/workspace/workspace/uid' => [ + [ + '_links' => [ + 'self' => [ + 'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/user/user', + ], + ], + 'uuid' => [ + ['value' => $author->uuid()], + ], + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/workspace/workspace', + ], + ], + ]; + } + +} diff --git a/core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php b/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceJsonAnonTest.php similarity index 86% rename from core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php rename to core/modules/workspaces/tests/src/Functional/Rest/WorkspaceJsonAnonTest.php index f44286f126..1763f3cf56 100644 --- a/core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceJsonAnonTest.php +++ b/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceJsonAnonTest.php @@ -1,6 +1,6 @@ [ - [ - 'value' => 'Running on faith', - ], - ], - ]; + return array_diff_key($this->getNormalizedPostEntity(), ['id' => TRUE]); } /** @@ -181,7 +175,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) { return "The 'view any workspace' permission is required."; break; case 'POST': - return "The 'create workspace' permission is required."; + return "The following permissions are required: 'administer workspaces' OR 'create workspace'."; break; case 'PATCH': return "The 'edit any workspace' permission is required."; diff --git a/core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php b/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php similarity index 91% rename from core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php rename to core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php index c87b438a23..d1b63c4e90 100644 --- a/core/modules/workspaces/tests/src/Functional/EntityResource/WorkspaceXmlAnonTest.php +++ b/core/modules/workspaces/tests/src/Functional/Rest/WorkspaceXmlAnonTest.php @@ -1,6 +1,6 @@