diff --git a/core/modules/user/tests/modules/user_access_test/user_access_test.module b/core/modules/user/tests/modules/user_access_test/user_access_test.module
index a5cdb4b3f5..978dde3b8c 100644
--- a/core/modules/user/tests/modules/user_access_test/user_access_test.module
+++ b/core/modules/user/tests/modules/user_access_test/user_access_test.module
@@ -15,21 +15,28 @@
  * Implements hook_ENTITY_TYPE_access() for entity type "user".
  */
 function user_access_test_user_access(User $entity, $operation, $account) {
+  $access = AccessResult::neutral();
   if ($entity->getAccountName() == "no_edit" && $operation == "update") {
     // Deny edit access.
-    return AccessResult::forbidden();
+    $access = AccessResult::forbidden();
   }
   if ($entity->getAccountName() == "no_delete" && $operation == "delete") {
     // Deny delete access.
-    return AccessResult::forbidden();
+    $access = AccessResult::forbidden();
   }
 
   // Account with role sub-admin can manage users with no roles.
   if (count($entity->getRoles()) == 1) {
     return AccessResult::allowedIfHasPermission($account, 'sub-admin');
   }
-
-  return AccessResult::neutral();
+  if ($entity->getAccountName() == "no_view_label" && $operation == "view label") {
+    // Deny view label access.
+    $access = AccessResult::forbidden();
+  }
+  if (isset($entity->testAccessAddCacheTags)) {
+    $access->addCacheTags($entity->testAccessAddCacheTags);
+  }
+  return $access;
 }
 
 /**
diff --git a/core/modules/user/tests/src/Kernel/UserTemplateTest.php b/core/modules/user/tests/src/Kernel/UserTemplateTest.php
new file mode 100644
index 0000000000..a216ae0aee
--- /dev/null
+++ b/core/modules/user/tests/src/Kernel/UserTemplateTest.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Drupal\Tests\user\Kernel;
+
+use Drupal\Core\Session\UserSession;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Component\Utility\Unicode;
+use Drupal\user\Entity\User;
+use Drupal\user\UserInterface;
+
+/**
+ * Tests template output for user module.
+ *
+ * @group user
+ */
+class UserTemplateTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['user', 'user_access_test'];
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * A user for testing.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('user');
+    $this->renderer = $this->container->get('renderer');
+    $this->user = User::create([
+      'uid' => 2,
+      'name' => $this->randomString(),
+    ]);
+  }
+
+  /**
+   * Tests whether the user can view user label.
+   */
+  public function testUsernameTemplateLabel() {
+    $build = [
+      '#theme' => 'username',
+      '#account' => $this->user,
+    ];
+    $this->setRawContent($this->renderer->renderRoot($build));
+    $this->assertEscaped($this->user->getDisplayName(), 'Account has permission to view user name.');
+  }
+
+  /**
+   * Tests whether the anonymous user name is always visible.
+   *
+   * Access control for label does not apply to non entities.
+   */
+  public function testUsernameTemplateLabelAnonymous() {
+    $account = new UserSession(['name' => $this->randomString()]);
+    $build = [
+      '#theme' => 'username',
+      '#account' => $account,
+    ];
+    $this->setRawContent($this->renderer->renderRoot($build));
+    $this->assertEscaped($account->getDisplayName(), 'Account can view anonymous user name.');
+  }
+
+  /**
+   * Tests whether the user cannot view user label.
+   */
+  public function testUsernameTemplateLabelAccessDenied() {
+    $this->user
+      // 'no_view_label' username disallows label viewing.
+      ->setUsername('no_view_label')
+      ->save();
+    $build = [
+      '#theme' => 'username',
+      '#account' => $this->user,
+    ];
+    $this->setRawContent($this->renderer->renderRoot($build));
+    $this->assertEscaped(t('- Restricted access -'), 'Account does not have permission to view user name.');
+  }
+
+  /**
+   * Tests whether long user names are truncated.
+   */
+  public function testUsernameTemplateTruncated() {
+    $username = $this->randomString(32);
+    $username_truncated = Unicode::truncate($username, 15, FALSE, TRUE);
+    $this->user
+      ->setUsername($username)
+      ->save();
+    $build = [
+      '#theme' => 'username',
+      '#account' => $this->user,
+    ];
+    $this->setRawContent($this->renderer->renderRoot($build));
+    $this->assertEscaped($username_truncated, 'Truncated user name found.');
+    $this->assertNoEscaped($username, 'Full user name not found.');
+  }
+
+  /**
+   * Tests whether long user names are truncated for anonymous accounts.
+   */
+  public function testUsernameTemplateTruncatedAnonymous() {
+    $username = $this->randomString(32);
+    $username_truncated = Unicode::truncate($username, 15, FALSE, TRUE);
+    $account = new UserSession(['name' => $username]);
+    $build = [
+      '#theme' => 'username',
+      '#account' => $account,
+    ];
+    $this->setRawContent($this->renderer->renderRoot($build));
+    $this->assertEscaped($username_truncated, 'Anonymous truncated user name found.');
+    $this->assertNoEscaped($username, 'Anonymous full user name not found.');
+  }
+
+  /**
+   * Tests the user name template is cached, and tags are present for user.
+   */
+  public function testUsernameTemplateCache() {
+    // Save the user once so it generates cache tags for itself.
+    $this->user->save();
+    $this->user->testAccessAddCacheTags = [$this->randomMachineName()];
+
+    $build['#cache']['keys'] = [$this->randomMachineName()];
+    $result_a = $this->renderUserName($build, $this->user);
+    $result_b = $this->renderUserName($build, $this->user);
+    $this->assertEquals($result_a, $result_b, 'User name not updated since it was cached.');
+
+    $result_a = $this->renderUserName([], $this->user);
+    $result_b = $this->renderUserName([], $this->user);
+    $this->assertNotEquals($result_a, $result_b, 'User name updated since it was not cached.');
+  }
+
+  /**
+   * Render a user name with a random string.
+   *
+   * @param array $build
+   *   The base element to build.
+   * @param \Drupal\user\UserInterface $user
+   *   A user entity.
+   *
+   * @return \Drupal\Component\Render\MarkupInterface
+   *   The rendered user name template.
+   */
+  protected function renderUserName(array $build, UserInterface $user) {
+    // Do not save the user, otherwise cache tags are invalidated.
+    $build['#theme'] = 'username';
+    $user->setUsername($this->randomMachineName());
+    $build['#account'] = $user;
+    $result = $this->renderer->renderRoot($build);
+
+    // Ensure cache metadata from user entity and access control added.
+    $expected_cache_tags = array_merge(
+      $user->testAccessAddCacheTags,
+      $user->getCacheTags()
+    );
+    sort($expected_cache_tags);
+    $this->assertEquals($expected_cache_tags, $build['#cache']['tags'], 'Cache tags found for user');
+    return $result;
+  }
+
+}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index a1395f4656..fcfb2e0504 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -10,7 +10,9 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Access\AccessibleInterface;
 use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
@@ -380,21 +382,35 @@ function template_preprocess_username(&$variables) {
     }
   }
 
-  // Set the name to a formatted name that is safe for printing and
-  // that won't break tables by being too long. Keep an unshortened,
-  // unsanitized version, in case other preprocess functions want to implement
-  // their own shortening logic or add markup. If they do so, they must ensure
-  // that $variables['name'] is safe for printing.
-  $name = $account->getDisplayName();
-  $variables['name_raw'] = $account->getAccountName();
-  if (mb_strlen($name) > 20) {
-    $name = Unicode::truncate($name, 15, FALSE, TRUE);
-    $variables['truncated'] = TRUE;
+  // Check the current user can view label, or allow if not an entity.
+  if ($account instanceof EntityInterface) {
+    $access = $account->access('view label', NULL, TRUE);
+    $access_label = $access->isAllowed();
+    $cacheability = new CacheableMetadata();
+    $cacheability
+      ->addCacheableDependency($access)
+      ->addCacheableDependency($account)
+      ->applyTo($variables);
+  }
+  else {
+    $access_label = TRUE;
+  }
+
+  if ($access_label) {
+    // Set the name to a formatted name that is safe for printing and that won't
+    // break tables by being too long. Keep an unshortened, unsanitized version,
+    // in case other preprocess functions want to implement their own shortening
+    // logic or add markup. If they do so, they must ensure that
+    // $variables['name'] is safe for printing.
+    $variables['name_raw'] = $account->getDisplayName();
+    $variables['truncated'] = mb_strlen($variables['name_raw']) > 20;
+    $variables['name'] = $variables['truncated'] ? Unicode::truncate($variables['name_raw'], 15, FALSE, TRUE) : $variables['name_raw'];
   }
   else {
     $variables['truncated'] = FALSE;
+    $variables['name'] = $variables['name_raw'] = t('- Restricted access -');
   }
-  $variables['name'] = $name;
+
   if ($account instanceof AccessibleInterface) {
     $variables['profile_access'] = $account->access('view');
   }
