reverted: --- b/core/modules/node/src/Tests/NodeAccessJoinTest.php +++ /dev/null @@ -1,351 +0,0 @@ - 'related_article', - 'entity_type' => 'node', - 'translatable' => FALSE, - 'entity_types' => [], - 'settings' => [ - 'target_type' => 'node', - ], - 'type' => 'entity_reference', - ]); - $field_storage->save(); - $field = FieldConfig::create([ - 'field_name' => 'related_article', - 'entity_type' => 'node', - 'bundle' => 'page', - 'label' => 'Related Article', - 'settings' => [ - 'handler' => 'default', - 'handler_settings' => [ - // Reference a single vocabulary. - 'target_bundles' => [ - 'article', - ], - ], - ], - ]); - $field->save(); - - entity_get_display('node', 'page', 'default') - ->setComponent('related_article') - ->save(); - entity_get_form_display('node', 'page', 'default') - ->setComponent('related_article', [ - 'type' => 'entity_reference_autocomplete', - ]) - ->save(); - - $field = FieldConfig::create([ - 'field_name' => 'related_article', - 'entity_type' => 'node', - 'bundle' => 'article', - 'label' => 'Related Article', - 'settings' => [ - 'handler' => 'default', - 'handler_settings' => [ - // Reference a single vocabulary. - 'target_bundles' => [ - 'article', - ], - ], - ], - ]); - $field->save(); - - entity_get_display('node', 'article', 'default') - ->setComponent('related_article') - ->save(); - entity_get_form_display('node', 'article', 'default') - ->setComponent('related_article', [ - 'type' => 'entity_reference_autocomplete', - ]) - ->save(); - - node_access_rebuild(); - \Drupal::state()->set('node_access_test.private', TRUE); - } - - /** - * Tests the accessibility of joined nodes. - * - * - Create two users with "access content" and "create article" permissions - * who can each access their own private articles but not others'. - * - Create article-type nodes with and without references to other articles. - * The articles and references represent all possible combinations of the - * tested access types. - * - Create page-type nodes referencing each of the articles, as well as a - * page with no reference. - * - Use a custom view that creates two joins between nodes and has a - * node_access tag. The view lists the page nodes, the article - * referenced by each page node, and the article referenced by each - * article. - * - * - Login with the author user and check that user does not have access to - * private nodes created by other users. Test access using total row - * count as well as checking for presence of individual page titles. - * - Repeat tests using a user with only the "access content" permission, - * confirming this user does not have access to any private nodes. - * - Repeat tests using a user with "access content" and "node test view" - * permissions, confirming this user sees the complete view. - */ - public function testNodeAccessJoin() { - - // User to add articles and test author access. - $this->authorUser = $this->drupalCreateUser(['access content', 'create article content']); - // Another user to add articles (whose private articles can not be accessed - // by authorUser). - $this->otherUser = $this->drupalCreateUser(['access content', 'create article content']); - - // Create the articles. - // The articles are stored in an array keyed by $article and $reference2, - // where - // $article is the access type of the article itself, and $reference2 is the - // access type of the reference linked to by the article. - // 'public' articles are created by otherUser with private=0. - // 'private' articles are created by otherUser with private=1. - // 'author_public' articles are created by authorUser with private=0. - // 'author_private' articles are created by authorUser with private=1. - // 'no_reference' is used for references when there is no related article. - foreach ( - [ - 'no_reference', - 'public', - 'private', - 'author_public', - 'author_private', - ] as $reference2) { - foreach (['public', 'private', 'author_public', 'author_private'] as $article) { - $is_author = (substr($article, 0, 6) == 'author'); - $is_private = (substr($article, -7) == 'private'); - $edit = [ - 'type' => 'article', - 'uid' => $is_author ? $this->authorUser->id() : $this->otherUser->id(), - ]; - $edit['private'][0]['value'] = $is_private; - // The article names provide the access status of the article and the - // access status of the related article (if any). - // The naming system ensures that the text 'Article $article' - // will only appear in the view if an article with that access type - // is displayed in the view. (The text - // '$article' alone will appear in the titles of other nodes - // that reference an article.) - $edit['title'] = "Article $article - $reference2"; - if ($reference2 != 'no_reference') { - $edit['related_article'][0]['target_id'] = $this->articles[$reference2]['no_reference']; - } - $node = $this->drupalCreateNode($edit); - $this->articles[$article][$reference2] = $node->id(); - - $this->assertEqual((int) $is_private, (int) $node->private->value, 'The private status of the article node was properly set in the node_access_test table.' . $node->uid->target_id); - if ($reference2 != 'no_reference') { - $this->assertEqual((int) $this->articles[$reference2]['no_reference'], (int) $node->related_article->target_id, 'Proper article attached to article.'); - } - } - } - - // Add a blank 'no_reference' entry to the article list, so that a page with - // no reference gets created. - $this->articles['no_reference']['no_reference'] = NULL; - - $total = 0; - $count_s_total = $count_s2_total = 0; - $count_s_public = $count_s2_public = 0; - $count_s_author = $count_s2_author = 0; - $total_public = $total_author = 0; - - // Create page nodes referencing each article, as a page without reference. - foreach ($this->articles as $reference => $list) { - foreach ($list as $reference2 => $article_nid) { - $title = "Page - $reference"; - if ($reference != 'no_reference') { - $title .= " - $reference2"; - } - $edit = [ - 'type' => 'page', - 'title' => $title, - ]; - if ($article_nid) { - $edit['related_article'][0]['target_id'] = $article_nid; - } - $node = $this->drupalCreateNode($edit); - if ($article_nid) { - $this->assertEqual((int) $article_nid, (int) $node->related_article->target_id, 'Proper article attached to page.'); - } - - // Calculate totals expected for each user type - // Total number of pages. - $total++; - // Total number of primary and secondary references. - if ($reference != 'no_reference') { - $count_s_total++; - if ($reference2 != 'no_reference') { - $count_s2_total++; - } - } - // Public users only see 'public' and 'author_public' articles. - if (substr($reference, -6) == 'public') { - $count_s_public++; - if (substr($reference2, -6) == 'public') { - $count_s2_public++; - } - } - // authorUser sees 'public','author_public', 'author_private' articles. - if (substr($reference, -6) == 'public' || substr($reference, 0, 6) == 'author') { - $count_s_author++; - if (substr($reference2, -6) == 'public' || substr($reference2, 0, 6) == 'author') { - $count_s2_author++; - } - } - - // $total_public and $total_author are not currently in use -- but - // represent the totals when joins are handled by adding an is-null - // check (i.e., if inaccessible references caused the entire row to be - // be hidden from view, instead of hiding just one cell of the table). - // Count of pages where all related articles are accessible by - // public users. - if (substr($reference, -7) != 'private' && substr($reference2, -7) != 'private') { - $total_public++; - } - // Count of pages where all related articles are accessible by - // authorUser. - if ($reference != 'private' && $reference2 != 'private') { - $total_author++; - } - } - } - - // Generate a view listing all the pages, and check the view's content for - // users with three different access levels. - ViewTestData::createTestViews(get_class($this), ['node_test_views']); - - // Check the author of the 'author' articles. - $this->drupalLogin($this->authorUser); - $this->drupalGet('test-node-access-join'); - $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); - $this->assertEqual($chk_total, $total, 'Author should see ' . $total . ' rows. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); - $this->assertEqual($chk_total, $count_s_author, 'Author should see ' . $count_s_author . ' primary references. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); - $this->assertEqual($chk_total, $count_s2_author, 'Author should see ' . $count_s2_author . ' secondary references. Actual: ' . $chk_total); - $this->assertText('Page - no_reference', 'Author should see page with no reference.'); - $this->assertText('Page - public - no_reference', 'Author should see page with one public reference.'); - $this->assertText('Page - public - public', 'Author should see page with two public references.'); - $this->assertText('Page - author_private - no_reference', 'Author should see page with own private reference.'); - $this->assertText('Article public', 'Author should see articles with access type: public.'); - $this->assertNoText('Article private', 'Author should not see articles with access type: private.'); - $this->assertText('Article author_public', 'Author should see articles with access type: author_public.'); - $this->assertText('Article author_private', 'Author should see articles with access type: author_private.'); - // Check a regular user (who did not author any articles). - $this->regularUser = $this->drupalCreateUser(['access content']); - $this->drupalLogin($this->regularUser); - $this->drupalGet('test-node-access-join'); - $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); - $this->assertEqual($chk_total, $total, 'Public user should see ' . $total . ' rows. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); - $this->assertEqual($chk_total, $count_s_public, 'Public user should see ' . $count_s_public . ' primary references. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); - $this->assertEqual($chk_total, $count_s2_public, 'Public user should see ' . $count_s2_public . ' secondary references. Actual: ' . $chk_total); - $this->assertText('Page - no_reference', 'Public user should see page with no reference.'); - $this->assertText('Page - public - no_reference', 'Public user should see page with one public reference.'); - $this->assertText('Page - public - public', 'Public user should see page with two public references.'); - $this->assertText('Article public', 'Public user should see articles with access type: public.'); - $this->assertNoText('Article private', 'Public user should not see articles with access type: private.'); - $this->assertText('Article author_public', 'Public user should see articles with access type: author_public.'); - $this->assertNoText('Article author_private', 'Public user should not see articles with access type: author_private.'); - // Check a user with the special 'node test view' permission, who should - // be able to view all pages and articles. - $this->accessUser = $this->drupalCreateUser(['access content', 'node test view']); - $this->drupalLogin($this->accessUser); - $this->drupalGet('test-node-access-join'); - $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); - $this->assertEqual($chk_total, $total, 'Full-access user should see ' . $total . ' rows. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); - $this->assertEqual($chk_total, $count_s_total, 'Full-access user should see ' . $count_s_total . ' primary references. Actual: ' . $chk_total); - $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); - $this->assertEqual($chk_total, $count_s2_total, 'Full-access user should see ' . $count_s2_total . ' secondary references. Actual: ' . $chk_total); - $this->assertText('Page - no_reference', 'Full-access user should see page with no reference.'); - $this->assertText('Page - public - no_reference', 'Full-access user should see page with one public reference.'); - $this->assertText('Page - public - public', 'Full-access user should see page with two public references.'); - $this->assertText('Page - author_private - no_reference', 'Full-access user should see page with author_private reference.'); - $this->assertText('Article public', 'Full-access user should see articles with access type: public.'); - $this->assertText('Article private', 'Full-access user should see articles with access type: private.'); - $this->assertText('Article author_public', 'Full-access user should see articles with access type: author_public.'); - $this->assertText('Article author_private', 'Full-access user should see articles with access type: author_private.'); - } - -} only in patch2: unchanged: --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeAccessJoinTest.php @@ -0,0 +1,363 @@ + 'related_article', + 'entity_type' => 'node', + 'translatable' => FALSE, + 'entity_types' => [], + 'settings' => [ + 'target_type' => 'node', + ], + 'type' => 'entity_reference', + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_name' => 'related_article', + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => 'Related Article', + 'settings' => [ + 'handler' => 'default', + 'handler_settings' => [ + // Reference a single vocabulary. + 'target_bundles' => [ + 'article', + ], + ], + ], + ]); + $field->save(); + + $entity_display = \Drupal::service('entity_display.repository'); + $entity_display->getViewDisplay('node', 'page', 'default') + ->setComponent('related_article') + ->save(); + $entity_display->getFormDisplay('node', 'page', 'default') + ->setComponent('related_article', [ + 'type' => 'entity_reference_autocomplete', + ]) + ->save(); + + $field = FieldConfig::create([ + 'field_name' => 'related_article', + 'entity_type' => 'node', + 'bundle' => 'article', + 'label' => 'Related Article', + 'settings' => [ + 'handler' => 'default', + 'handler_settings' => [ + // Reference a single vocabulary. + 'target_bundles' => [ + 'article', + ], + ], + ], + ]); + $field->save(); + + $entity_display->getViewDisplay('node', 'article', 'default') + ->setComponent('related_article') + ->save(); + $entity_display->getFormDisplay('node', 'article', 'default') + ->setComponent('related_article', [ + 'type' => 'entity_reference_autocomplete', + ]) + ->save(); + + node_access_rebuild(); + \Drupal::state()->set('node_access_test.private', TRUE); + } + + /** + * Tests the accessibility of joined nodes. + * + * - Create two users with "access content" and "create article" permissions + * who can each access their own private articles but not others'. + * - Create article-type nodes with and without references to other articles. + * The articles and references represent all possible combinations of the + * tested access types. + * - Create page-type nodes referencing each of the articles, as well as a + * page with no reference. + * - Use a custom view that creates two joins between nodes and has a + * node_access tag. The view lists the page nodes, the article + * referenced by each page node, and the article referenced by each + * article. + * + * - Login with the author user and check that user does not have access to + * private nodes created by other users. Test access using total row + * count as well as checking for presence of individual page titles. + * - Repeat tests using a user with only the "access content" permission, + * confirming this user does not have access to any private nodes. + * - Repeat tests using a user with "access content" and "node test view" + * permissions, confirming this user sees the complete view. + */ + public function testNodeAccessJoin() { + + // User to add articles and test author access. + $this->authorUser = $this->drupalCreateUser([ + 'access content', + 'create article content', + ]); + // Another user to add articles (whose private articles can not be accessed + // by authorUser). + $this->otherUser = $this->drupalCreateUser([ + 'access content', + 'create article content', + ]); + + // Create the articles. + // The articles are stored in an array keyed by $article and $reference2, + // where + // $article is the access type of the article itself, and $reference2 is the + // access type of the reference linked to by the article. + // 'public' articles are created by otherUser with private=0. + // 'private' articles are created by otherUser with private=1. + // 'author_public' articles are created by authorUser with private=0. + // 'author_private' articles are created by authorUser with private=1. + // 'no_reference' is used for references when there is no related article. + foreach ( + [ + 'no_reference', + 'public', + 'private', + 'author_public', + 'author_private', + ] as $reference2) { + foreach ([ + 'public', + 'private', + 'author_public', + 'author_private', + ] as $article) { + $is_author = (substr($article, 0, 6) == 'author'); + $is_private = (substr($article, -7) == 'private'); + $edit = [ + 'type' => 'article', + 'uid' => $is_author ? $this->authorUser->id() : $this->otherUser->id(), + ]; + $edit['private'][0]['value'] = $is_private; + // The article names provide the access status of the article and the + // access status of the related article (if any). + // The naming system ensures that the text 'Article $article' + // will only appear in the view if an article with that access type + // is displayed in the view. (The text + // '$article' alone will appear in the titles of other nodes + // that reference an article.) + $edit['title'] = "Article $article - $reference2"; + if ($reference2 != 'no_reference') { + $edit['related_article'][0]['target_id'] = $this->articles[$reference2]['no_reference']; + } + $node = $this->drupalCreateNode($edit); + $this->articles[$article][$reference2] = $node->id(); + + $this->assertEquals((int) $is_private, (int) $node->private->value, 'The private status of the article node was properly set in the node_access_test table.' . $node->uid->target_id); + if ($reference2 != 'no_reference') { + $this->assertEquals((int) $this->articles[$reference2]['no_reference'], (int) $node->related_article->target_id, 'Proper article attached to article.'); + } + } + } + + // Add a blank 'no_reference' entry to the article list, so that a page with + // no reference gets created. + $this->articles['no_reference']['no_reference'] = NULL; + + $total = 0; + $count_s_total = $count_s2_total = 0; + $count_s_public = $count_s2_public = 0; + $count_s_author = $count_s2_author = 0; + $total_public = $total_author = 0; + + // Create page nodes referencing each article, as a page without reference. + foreach ($this->articles as $reference => $list) { + foreach ($list as $reference2 => $article_nid) { + $title = "Page - $reference"; + if ($reference != 'no_reference') { + $title .= " - $reference2"; + } + $edit = [ + 'type' => 'page', + 'title' => $title, + ]; + if ($article_nid) { + $edit['related_article'][0]['target_id'] = $article_nid; + } + $node = $this->drupalCreateNode($edit); + if ($article_nid) { + $this->assertEquals((int) $article_nid, (int) $node->related_article->target_id, 'Proper article attached to page.'); + } + + // Calculate totals expected for each user type + // Total number of pages. + $total++; + // Total number of primary and secondary references. + if ($reference != 'no_reference') { + $count_s_total++; + if ($reference2 != 'no_reference') { + $count_s2_total++; + } + } + // Public users only see 'public' and 'author_public' articles. + if (substr($reference, -6) == 'public') { + $count_s_public++; + if (substr($reference2, -6) == 'public') { + $count_s2_public++; + } + } + // authorUser sees 'public','author_public', 'author_private' articles. + if (substr($reference, -6) == 'public' || substr($reference, 0, 6) == 'author') { + $count_s_author++; + if (substr($reference2, -6) == 'public' || substr($reference2, 0, 6) == 'author') { + $count_s2_author++; + } + } + + // $total_public and $total_author are not currently in use -- but + // represent the totals when joins are handled by adding an is-null + // check (i.e., if inaccessible references caused the entire row to be + // be hidden from view, instead of hiding just one cell of the table). + // Count of pages where all related articles are accessible by + // public users. + if (substr($reference, -7) != 'private' && substr($reference2, -7) != 'private') { + $total_public++; + } + // Count of pages where all related articles are accessible by + // authorUser. + if ($reference != 'private' && $reference2 != 'private') { + $total_author++; + } + } + } + + // Generate a view listing all the pages, and check the view's content for + // users with three different access levels. + ViewTestData::createTestViews(get_class($this), ['node_test_views']); + + // Check the author of the 'author' articles. + $this->drupalLogin($this->authorUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEquals($chk_total, $total, 'Author should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); + $this->assertEquals($chk_total, $count_s_author, 'Author should see ' . $count_s_author . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); + $this->assertEquals($chk_total, $count_s2_author, 'Author should see ' . $count_s2_author . ' secondary references. Actual: ' . $chk_total); + + $session = $this->assertSession(); + $session->pageTextContains('Page - no_reference'); + $session->pageTextContains('Page - public - no_reference'); + $session->pageTextContains('Page - public - public'); + $session->pageTextContains('Page - author_private - no_reference'); + $session->pageTextContains('Article public'); + $session->pageTextNotContains('Article private'); + $session->pageTextContains('Article author_public'); + $session->pageTextContains('Article author_private'); + // Check a regular user (who did not author any articles). + $this->regularUser = $this->drupalCreateUser(['access content']); + $this->drupalLogin($this->regularUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEquals($chk_total, $total, 'Public user should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); + $this->assertEquals($chk_total, $count_s_public, 'Public user should see ' . $count_s_public . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); + $this->assertEquals($chk_total, $count_s2_public, 'Public user should see ' . $count_s2_public . ' secondary references. Actual: ' . $chk_total); + $session->pageTextContains('Page - no_reference'); + $session->pageTextContains('Page - public - no_reference'); + $session->pageTextContains('Page - public - public'); + $session->pageTextContains('Article public'); + $session->pageTextNotContains('Article private'); + $session->pageTextContains('Article author_public'); + $session->pageTextNotContains('Article author_private'); + // Check a user with the special 'node test view' permission, who should + // be able to view all pages and articles. + $this->accessUser = $this->drupalCreateUser([ + 'access content', + 'node test view', + ]); + $this->drupalLogin($this->accessUser); + $this->drupalGet('test-node-access-join'); + $chk_total = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertEquals($chk_total, $total, 'Full-access user should see ' . $total . ' rows. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a")); + $this->assertEquals($chk_total, $count_s_total, 'Full-access user should see ' . $count_s_total . ' primary references. Actual: ' . $chk_total); + $chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a")); + $this->assertEquals($chk_total, $count_s2_total, 'Full-access user should see ' . $count_s2_total . ' secondary references. Actual: ' . $chk_total); + $session->pageTextContains('Page - no_reference'); + $session->pageTextContains('Page - public - no_reference'); + $session->pageTextContains('Page - public - public'); + $session->pageTextContains('Page - author_private - no_reference'); + $session->pageTextContains('Article public'); + $session->pageTextContains('Article private'); + $session->pageTextContains('Article author_public'); + $session->pageTextContains('Article author_private'); + } + +} only in patch2: unchanged: --- a/core/modules/node/tests/src/Kernel/NodeAccessTest.php +++ b/core/modules/node/tests/src/Kernel/NodeAccessTest.php @@ -123,4 +123,24 @@ public function testUnsupportedOperation() { $this->assertNodeAccess(['random_operation' => FALSE], $node, $web_user); } + /** + * Test node grants for queries with node access checks and base table join. + */ + public function testQueryWithBaseTableJoin() { + $this->enableModules(['node_access_test_empty']); + $this->drupalCreateNode(['type' => 'page']); + + $container = \Drupal::getContainer(); + $container->get('current_user')->setAccount($this->drupalCreateUser()); + + $query = \Drupal::database()->select('node_field_data', 'n'); + // Intentionally add a left join of the base table on the base table with a + // failing condition. This can, for example, happen in views with non + // required relations. + $query->leftJoin('node_field_data', 'nc', 'n.changed = nc.nid'); + $query->addTag('node_access'); + + $this->assertEquals(1, $query->countQuery()->execute()->fetchField()); + } + }