diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index 6b6f280..6b9cb25 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -190,7 +190,16 @@ public function alterQuery($query, array $tables, $op, AccountInterface $account // Now handle entities. $subquery->where("$nalias.$field = na.nid"); - $query->exists($subquery); + if (empty($tableinfo['join type'])) { + $query->exists($subquery); + } + else { + // If it's a join allow for entity_id to also be null. + $subquery_cond = db_or(); + $subquery_cond->exists($subquery); + $subquery_cond->isNull("$nalias.$field"); + $query->condition($subquery_cond); + } } } } diff --git a/core/modules/node/src/Tests/NodeAccessJoinTest.php b/core/modules/node/src/Tests/NodeAccessJoinTest.php new file mode 100644 index 0000000..19d1050 --- /dev/null +++ b/core/modules/node/src/Tests/NodeAccessJoinTest.php @@ -0,0 +1,299 @@ + '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 "private" node access functionality. + * + * - Create user with "access content" and "create article" permissions which + * will be author having access to some private articles but not others. + * - Create articles with and without reference for the "author" user and + * anonymous user. + * - Create pages with articles as reference for any combination of article + * (private or private by author, reference being both private both public, + * etc.). + * + * - Login with author and check that it doesn't have access to private nodes + * he is not the author of. + * - Use the page titles to be sure the user sees what it should see and that + * results count is not just a fluke. + * - Create and login with user with only "access content" permission. + * - Test that he sees only public nodes. Use the page titles to be sure the + * user sees what it should see and that results count is not just a fluke. + * - Create and login with user with "access content" and "node test view" + * permissions. + * - Test that the user sees all the rows. + */ + function testNodeAccessJoin() { + + // User to add articles and test author access. + $this->authorUser = $this->drupalCreateUser(['access content', 'create article content']); + + foreach (['no reference', 'public', 'private'] as $reference) { + foreach ([0 => 'public', 1 => 'private'] as $is_private => $type) { + $edit = [ + 'title' => t('author @private_public article - @reference', ['@private_public' => $type, '@reference' => ($reference != 'no reference') ? 'author ' . $reference : $reference]), + 'type' => 'article', + 'uid' => $this->authorUser->id(), + ]; + if ($is_private) { + $edit['private'][0]['value'] = TRUE; + } + else { + $edit['private'][0]['value'] = FALSE; + } + if ($reference != 'no reference') { + $edit['related_article'][0]['target_id'] = $this->articles['author no reference']['author ' . $reference]; + } + + $node = $this->drupalCreateNode($edit); + $this->assertEqual($is_private, (int)$node->private->value, 'The private status of the node was properly set in the node_access_test table.' . $node->uid->target_id); + $this->articles['author ' . $reference]['author ' . $type] = $node->id(); + if ($reference != 'no reference') { + $this->assertEqual((int)$this->articles['author no reference']['author ' . $reference], (int)$node->related_article->target_id, 'Proper article attached to article.'); + } + + $edit = [ + 'title' => t('@private_public article - @reference', ['@private_public' => $type, '@reference' => $reference]), + 'type' => 'article', + ]; + if ($is_private) { + $edit['private'][0]['value'] = TRUE; + } + else { + $edit['private'][0]['value'] = FALSE; + } + if ($reference != 'no reference') { + $edit['related_article'][0]['target_id'] = $this->articles['no reference'][$reference]; + } + + $node = $this->drupalCreateNode($edit); + $this->assertEqual($is_private, (int)$node->private->value, 'The private status of the node was properly set in the node_access_test table.' . $node->uid->target_id); + $this->articles[$reference][$type] = $node->id(); + if ($reference != 'no reference') { + $this->assertEqual((int)$this->articles['no reference'][$reference], (int)$node->related_article->target_id, 'Proper article attached to article.'); + } + } + } + + $edit = [ + 'type' => 'page', + 'title' => 'Page - no reference', + ]; + $this->drupalCreateNode($edit); + $total = 1; + $total_public = 1; + $total_author = 0; + foreach ([0 => 'public', 1 => 'private'] as $is_private => $type) { + foreach (['no reference', 'public', 'private'] as $reference) { + foreach (['author', 'regular'] as $author) { + foreach (['author', 'regular'] as $author1) { + if ($author == 'author') { + $title = 'author '; + } + else { + $title = ''; + } + if ($author1 == 'author') { + $title1 = 'author '; + } + else { + $title1 = ''; + } + if (isset($this->articles[$title . $reference]) && isset($this->articles[$title . $reference][$title1 . $type])) { + $edit['title'] = t('Page - @title1@private_public - @title2@reference', ['@private_public' => $type, '@reference' => $reference, '@title1' => $title, '@title2' => ($reference != 'no reference') ? $title1 : '']); + $edit['related_article'][0]['target_id'] = $this->articles[$title . $reference][$title1 . $type]; + + $node = $this->drupalCreateNode($edit); + $total++; + if (($type == 'private' || $reference == 'private') && ($author == 'author' || $author1 == 'author')) { + $total_author++; + } + if ($type != 'private' && $reference != 'private') { + $total_public++; + } + $this->assertEqual((int)$this->articles[$title . $reference][$title1 . $type], (int)$node->related_article->target_id, 'Proper article attached to page.'); + } + } + } + } + } + $total_author += $total_public; + + ViewTestData::createTestViews(get_class($this), ['node_test_views']); + + // User to check if entries with private references are hidden, but that his + // own private references are visible. + $this->drupalLogin($this->authorUser); + $this->drupalGet('test-node-access-join'); + $this->verbose('Viewing page with author'); + $this->assertText('Page - no reference', 'Node with null reference visible'); + $this->assertText('Page - public - no reference', 'Node with public reference visible'); + $this->assertText('Page - public - public', 'Node with 2 public joins visible'); + $this->assertText('Page - author private - no reference', "Node with author's private reference visible"); + $this->assertNoText('- private', "Node author doesn't see private nodes he's not the author of"); + $this->assertText('Page - public - no reference', 'Node with public reference visible'); + $rows = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertTrue($rows == $total_author, "Author sees all rows he has access to. $rows - $total_author"); + + // User to check if entries with private references are hidden. + $this->regularUser = $this->drupalCreateUser(['access content']); + $this->drupalLogin($this->regularUser); + $this->drupalGet('test-node-access-join'); + $this->verbose('Viewing page with regular user'); + $this->assertText('Page - no reference', 'Node with null reference visible'); + $this->assertNoText('private', "Node with private reference hidden"); + $this->assertText('Page - public - no reference', 'Node with public reference visible'); + $this->assertText('Page - public - public', 'Node with 2 public joins visible'); + $rows = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertTrue($rows == $total_public, "User with no access sees only public rows."); + + // User to check if entries with private references are visible. + $this->accessUser = $this->drupalCreateUser(['access content', 'node test view']); + $this->drupalLogin($this->accessUser); + $this->drupalGet('test-node-access-join'); + $this->verbose('Viewing page with access user'); + $rows = count($this->xpath("//td[@headers='view-title-table-column']")); + $this->assertTrue($rows == $total, "User with access sees all rows."); + + } + +} diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module index e195ea8..1338eac 100644 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.module +++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module @@ -78,7 +78,7 @@ function node_access_test_node_grants($account, $op) { function node_access_test_node_access_records(NodeInterface $node) { $grants = array(); // For NodeAccessBaseTableTestCase, only set records for private nodes. - if (!\Drupal::state()->get('node_access_test.private') || $node->private->value) { + if (!\Drupal::state()->get('node_access_test.private') || (isset($node->private) && $node->private->value)) { // Groups 8888 and 8889 for the node_access_test realm both receive a view // grant for all controlled nodes. See node_access_test_node_grants(). $grants[] = array( diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml new file mode 100644 index 0000000..be876e0 --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml @@ -0,0 +1,339 @@ +uuid: 0a62a0f4-8322-49c3-964d-770997ac5ac0 +langcode: en +status: true +dependencies: + config: + - node.type.page + module: + - node + - user +id: test_node_access_join +label: 'Test Node Access Join' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 1000 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: table + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + title_1: + id: title_1 + table: node_field_data + field: title + relationship: related_article + group_type: group + admin_label: '' + label: Article + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: 'No article referenced' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + title_2: + id: title_2 + table: node_field_data + field: title + relationship: related_article_1 + group_type: group + admin_label: '' + label: 'Article 1' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + page: page + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: title + plugin_id: standard + title: 'Test Reference Access' + header: { } + footer: { } + empty: { } + relationships: + related_article: + id: related_article + table: node__related_article + field: related_article + relationship: none + group_type: group + admin_label: 'Page related article' + required: false + plugin_id: standard + related_article_1: + id: related_article_1 + table: node__related_article + field: related_article + relationship: related_article + group_type: group + admin_label: 'Article related article' + required: false + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-node-access-join + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { }