diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index b05de53a0f..67eb632b62 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -443,6 +443,8 @@ Views Workflows - Sam Becker 'Sam152' https://www.drupal.org/u/sam152 +Workspaces +- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu Topic maintainers ----------------- 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 9513319a62..ce0765a272 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -2001,6 +2001,13 @@ public function testPostIndividual() { $this->assertSame($created_entity_document, $decoded_response_body); // Assert that the entity was indeed created using the POSTed values. foreach ($this->getPostDocument()['data']['attributes'] as $field_name => $field_normalization) { + // Entity types with string IDs have to send the entity ID when creating + // the document, but our normalization excludes it from the list of data + // attributes. + if ($field_name === $created_entity->getEntityType()->getKey('id')) { + continue; + } + // If the value is an array of properties, only verify that the sent // properties are present, the server could be computing additional // properties. 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..a3ad8f14c9 --- /dev/null +++ b/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php @@ -0,0 +1,197 @@ + NULL, + ]; + + /** + * {@inheritdoc} + */ + protected static $firstCreatedEntityId = 'updated_campaign'; + + /** + * {@inheritdoc} + */ + protected static $secondCreatedEntityId = 'updated_campaign'; + + /** + * {@inheritdoc} + * + * @var \Drupal\workspaces\WorkspaceInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected function setUpAuthorization($method) { + $this->grantPermissionsToTestedRole(['administer workspaces']); + } + + /** + * {@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' => [ + 'id' => 'updated_campaign', + 'label' => 'Updated campaign', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getPatchDocument() { + $patch_document = parent::getPatchDocument(); + unset($patch_document['data']['attributes']['id']); + return $patch_document; + } + + /** + * {@inheritdoc} + */ + protected function getModifiedEntityForPostTesting() { + $modified = parent::getModifiedEntityForPostTesting(); + $modified['data']['attributes']['id'] = strtolower($this->randomMachineName()); + return $modified; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + switch ($method) { + case 'POST': + return "The following permissions are required: 'administer workspaces' OR 'create workspace'."; + case 'GET': + case 'PATCH': + case 'DELETE': + return parent::getExpectedUnauthorizedAccessMessage($method); + } + } + + /** + * {@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..90f759f188 100644 --- a/core/modules/workspaces/src/EntityTypeInfo.php +++ b/core/modules/workspaces/src/EntityTypeInfo.php @@ -86,6 +86,9 @@ 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. + $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..a39e904a88 100644 --- a/core/modules/workspaces/src/WorkspaceAccessControlHandler.php +++ b/core/modules/workspaces/src/WorkspaceAccessControlHandler.php @@ -19,23 +19,23 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler { */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { /** @var \Drupal\workspaces\WorkspaceInterface $entity */ - if ($account->hasPermission('administer workspaces')) { - return AccessResult::allowed()->cachePerPermissions(); - } + $access_result = AccessResult::allowedIfHasPermission($account, 'administer workspaces'); // @todo Consider adding explicit "publish any|own workspace" permissions in // https://www.drupal.org/project/drupal/issues/3084260. $permission_operation = ($operation === 'update' || $operation === 'publish') ? 'edit' : $operation; // Check if the user has permission to access all workspaces. - $access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' any workspace'); + $any_access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' any workspace'); + $access_result->orIf($any_access_result); // Check if it's their own workspace, and they have permission to access // their own workspace. if ($access_result->isNeutral() && $account->isAuthenticated() && $account->id() === $entity->getOwnerId()) { - $access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' own workspace') + $own_access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' own workspace') ->cachePerUser() ->addCacheableDependency($entity); + $access_result->orIf($own_access_result); } return $access_result; @@ -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]); } /** 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 @@