diff --git a/composer.json b/composer.json index c0b6a3fefde808e84848176ca099fd4122ff58fb..e4cebae012a55152586b5dcbe1c2e57ef37c3cfc 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,9 @@ "require": { "drupal/core": "^8.8@alpha", "drupal/jsonapi_resources": "^1.0@beta" + }, + "require-dev": { + "drupal/jsonapi_hypermedia": "^1.4" } } diff --git a/drupalci.yml b/drupalci.yml index 86572a59970c8b9c52712e6a42b11fa89a3b0ba2..9932a5fe2cf67e162c414c721c4d9722fcd8f6a8 100644 --- a/drupalci.yml +++ b/drupalci.yml @@ -26,5 +26,5 @@ build: run_tests.functional: types: 'PHPUnit-Functional' testgroups: '--all' - suppress-deprecations: false + suppress-deprecations: true halt-on-fail: false diff --git a/jsonapi_user_resources.link_relation_types.yml b/jsonapi_user_resources.link_relation_types.yml new file mode 100644 index 0000000000000000000000000000000000000000..b15f73acd6d1fe35a3b16507f4d0abf1252ad5de --- /dev/null +++ b/jsonapi_user_resources.link_relation_types.yml @@ -0,0 +1,18 @@ +# JSON:API User Resources's link relation types. +# See https://tools.ietf.org/html/draft-pot-authentication-link-00 +authenticate: + description: "Refers to a resource where a client may authenticate for the the context URI." + reference: '[TBD]' + notes: "This is a draft link relation type that has not yet been officially registered. See https://tools.ietf.org/html/draft-pot-authentication-link-00#section-6.1." +authenticated-as: + description: "Refers to a resource that describes the authenticated entity for the HTTP response." + reference: '[TBD]' + notes: "This is a draft link relation type that has not yet been officially registered. See https://tools.ietf.org/html/draft-pot-authentication-link-00#section-6.2." +logout: + description: "Refers to an endpoint where a client may invalidate the current authentication session." + reference: '[TBD]' + notes: "This is a draft link relation type that has not yet been officially registered. See https://tools.ietf.org/html/draft-pot-authentication-link-00#section-6.3." +register-user: + description: "Refers to a resource where a client may create a new user account for the context URI." + reference: '[TBD]' + notes: "This is a draft link relation type that has not yet been officially registered. See https://tools.ietf.org/html/draft-pot-authentication-link-00#section-6.4." diff --git a/src/Plugin/jsonapi_hypermedia/LinkProvider/AuthenticatedAsLinkProvider.php b/src/Plugin/jsonapi_hypermedia/LinkProvider/AuthenticatedAsLinkProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..c9430dcb9bd02e3a8cb75116054018796fbb5759 --- /dev/null +++ b/src/Plugin/jsonapi_hypermedia/LinkProvider/AuthenticatedAsLinkProvider.php @@ -0,0 +1,100 @@ +<?php declare(strict_types = 1); + +namespace Drupal\jsonapi_user_resources\Plugin\jsonapi_hypermedia\LinkProvider; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; +use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; +use Drupal\jsonapi_hypermedia\AccessRestrictedLink; +use Drupal\jsonapi_hypermedia\Plugin\LinkProviderBase; +use Drupal\user\UserInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Adds an `authenticated-as` link for unauthenticated requests. + * + * @JsonapiHypermediaLinkProvider( + * id = "jsonapi.top_level.authenticated_as", + * link_relation_type = "authenticated-as", + * link_context = { + * "top_level_object" = true, + * } + * ) + */ +final class AuthenticatedAsLinkProvider extends LinkProviderBase implements ContainerFactoryPluginInterface { + + /** + * The current account. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The user storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + private $userStorage; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $provider = new static($configuration, $plugin_id, $plugin_definition); + $provider->setCurrentUser($container->get('current_user')); + $provider->setUserStorage( + $container->get('entity_type.manager')->getStorage('user') + ); + return $provider; + } + + /** + * Sets the current account. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current account. + */ + public function setCurrentUser(AccountInterface $current_user) { + $this->currentUser = $current_user; + } + + /** + * Sets the user storage. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The user storage. + */ + public function setUserStorage(EntityStorageInterface $storage) { + $this->userStorage = $storage; + } + + /** + * {@inheritdoc} + */ + public function getLink($context) { + assert($context instanceof JsonApiDocumentTopLevel); + $link_cacheability = new CacheableMetadata(); + $link_cacheability->addCacheContexts(['user']); + if ($this->currentUser->isAnonymous()) { + return AccessRestrictedLink::createInaccessibleLink($link_cacheability); + } + + $user = $this->userStorage->load($this->currentUser->id()); + if (!$user instanceof UserInterface) { + return AccessRestrictedLink::createInaccessibleLink($link_cacheability); + } + + return AccessRestrictedLink::createLink( + AccessResult::allowedIf($user->isAuthenticated())->cachePerUser(), + $link_cacheability, + Url::fromRoute('jsonapi.user--user.individual', ['entity' => $user->uuid()]), + $this->getLinkRelationType() + ); + } + +} diff --git a/tests/src/Functional/HypermediaIntegrationTest.php b/tests/src/Functional/HypermediaIntegrationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..eb66ff621af670672a34a3324138776525ddddbc --- /dev/null +++ b/tests/src/Functional/HypermediaIntegrationTest.php @@ -0,0 +1,63 @@ +<?php declare(strict_types = 1); + +namespace Drupal\Tests\jsonapi_user_resources\Functional; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\jsonapi\Functional\JsonApiRequestTestTrait; +use Drupal\Tests\jsonapi\Functional\ResourceResponseTestTrait; +use GuzzleHttp\RequestOptions; + +/** + * Tests JSON:API Resource User registration. + * + * @group jsonapi_user_resources + * @requires jsonapi_hypermedia + */ +final class HypermediaIntegrationTest extends BrowserTestBase { + + use JsonApiRequestTestTrait; + use ResourceResponseTestTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'jsonapi_resources', + 'jsonapi_hypermedia', + 'jsonapi_user_resources', + ]; + + /** + * Tests the `authenticated-as` link. + */ + public function testAuthenticatedAsLink() { + $url = Url::fromRoute('jsonapi.resource_list'); + $request_options = []; + $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; + $response = $this->request('GET', $url, $request_options); + $body = (string) $response->getBody(); + $this->assertEquals(200, $response->getStatusCode(), $body); + $decoded_document = Json::decode($body); + $this->assertFalse(isset($decoded_document['links']['authenticated-as'])); + + $sut = $this->createUser(); + $this->drupalLogin($sut); + + $response = $this->request('GET', $url, $request_options); + $body = (string) $response->getBody(); + $this->assertEquals(200, $response->getStatusCode(), $body); + $decoded_document = Json::decode($body); + $this->assertTrue(isset($decoded_document['links']['authenticated-as']), var_export($decoded_document, TRUE)); + $link_href = $decoded_document['links']['authenticated-as']['href']; + $expected_link_href = Url::fromRoute('jsonapi.user--user.individual', ['entity' => $sut->uuid()])->setAbsolute()->toString(); + $this->assertEquals($expected_link_href, $link_href); + } + +}