Hello,
I want to report a behaviour that seems strange for me.

1. I created a test Webform having a single text input element.
2. I submitted several submissions of the above webform as admin or as another user (lets say user1)
3. I have created a corresponding Webform View (and Display page) for the above webform and gave access to user2.

user2 is able to see only the webform metadata and not the actual webform data. I have also implemented an user exposed filter: if a enter "known" data (previously submitted) it limits the number of rows.

The only solution was to give user2 View any submissions rights for the Webform.

Is this the expected behaviour, or should the webform data be displayed to all users/roles having access to the View?

Regards,
Sorin

CommentFileSizeAuthor
#28 3006765-entity-access-in-view-query-28.patch40.83 KBbucefal91
#28 interdiff-26-28.txt3.89 KBbucefal91
#26 interdiff-25-26.txt6.62 KBbucefal91
#26 3006765-entity-access-in-view-query-26.patch41.09 KBbucefal91
#25 3006765-25.patch41.25 KBjrockowitz
#25 interdiff-3006765-24-25.txt10.99 KBjrockowitz
#24 interdiff-3006765-23-24.txt15.15 KBjrockowitz
#24 3006765-24.patch38.21 KBjrockowitz
#23 3006765-23.patch34.69 KBjrockowitz
#23 interdiff-3006765-16-23.txt30.69 KBjrockowitz
#16 3006765-entity-access-in-view-query-16.patch34.23 KBbucefal91
#16 interdiff-14-16.txt633 bytesbucefal91
#14 interdiff-12-14.txt2.82 KBbucefal91
#14 3006765-entity-access-in-view-query-14.patch33.61 KBbucefal91
#12 3006765-entity-access-in-view-query-12.patch31.17 KBbucefal91
#12 interdiff-10-12.txt3.28 KBbucefal91
#10 interdiff-6-10.txt469 bytesbucefal91
#10 3006765-entity-access-in-view-query-10.patch28.08 KBbucefal91
#8 3006765-entity-access-in-view-query-6.patch27.62 KBbucefal91
#8 interdiff-4-6.txt6.38 KBbucefal91
#6 interdiff-4-6.txt6.38 KBbucefal91
#6 3006765-entity-access-in-view-query-6.patch27.62 KBbucefal91
#4 3006765-entity-access-in-view-query-4.patch13.38 KBbucefal91
#2 Screenshot from 2018-10-18 17-22-20.png128.41 KBbucefal91
#2 Screenshot from 2018-10-18 17-22-22.png110.05 KBbucefal91
2018-10-15-180808_1390x1056_scrot.png79.02 KBsorin_cocorada
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sorin_cocorada created an issue. See original summary.

bucefal91’s picture

Title: Webform data fields not visible inside Webform View » Webform submission entity access is not enforced in view query
Project: Webform Views Integration » Webform
Version: 8.x-5.0-alpha5 » 8.x-5.x-dev
FileSize
110.05 KB
128.41 KB

Oh lord, you stamped upon a big chunk of work. Let me transfer this ticket to the webform issue queue because that's where it must be dealt with. (I am a co-maintainer of Webform too, so it doesn't mean I am just ditching you :) )

Jacob, there is basically access bypass when we run a views query against webform_submission table. Let me describe what I was able to find out.

Let's first consider how Views run a query against Node entities. The NodeViewsData defines the property 'access query tag' on the 'node' base table:

  public function getViewsData() {
    $data = parent::getViewsData();

//  .................
    $data['node_field_data']['table']['base']['access query tag'] = 'node_access';
// .................
  }

This property is then picked up by Drupal\views\Plugin\views\query::execute():

  public function execute(ViewExecutable $view) {
// ....................

    if (empty($this->options['disable_sql_rewrite'])) {
      $base_table_data = Views::viewsData()->get($this->view->storage->get('base_table'));
      if (isset($base_table_data['table']['base']['access query tag'])) {
        $access_tag = $base_table_data['table']['base']['access query tag'];
        $query->addTag($access_tag);
        $count_query->addTag($access_tag);
      }
    }
// ...........................
  }

So then node_query_node_access_alter() hook gets invoked and that's where they supply necessary additional WHERE clauses to filter out nodes which are unaccessible for current user. I thought D8 was doing that automatically but it actually seems like it's the responsibility of the module that provides a new entity type to enforce the access on SQL level that way.

Apparently, in the case of webform_submission nothing of the above happens, so the view successfully queries all the submissions independently of user access.

You can try building some simple view.. you do not even need Webform Views, just throw in a 'completed' field, for example. Then create such a user that does not have access to a submission (i.e. he should face access denied page if he opens /admin/structure/webform/manage/[webform-id]/submission/[webform-submission-id]). Then navigate to the view being logged in as such user and observe the 'completed' field successfully displayed.

See the 2 screenshots I am attaching. I can volunteer myself for the patch. I have a pretty clear picture by now of how to do and what goes wrong. I'll make sure to put up tests too to make sure it stays covered.

jrockowitz’s picture

@bucefal91 I definitely need and appreciate your help with this issue. You have a much greater understanding of Views. When possible please provide comments and references in your patch. It helps to know which code from core is being used to inspire your solution. Thanks.

bucefal91’s picture

Status: Active » Needs review
FileSize
13.38 KB

Jacob, here are 2 tests that pin point the issue. They will fail now because access enforcement is not made in views query.

There are 2 actually similar issues:

  • Access is not enforced via access rules
  • and access is not enforced via user's permissions

hence the 2 tests instead of 1.

I will try to 'walk through' this ticket on my training in Atlanta the day after tomorrow :)

Status: Needs review » Needs work

The last submitted patch, 4: 3006765-entity-access-in-view-query-4.patch, failed testing. View results

bucefal91’s picture

Status: Needs work » Needs review
FileSize
27.62 KB
6.38 KB

Jake :) hi!

Today I had time to review and polish the patch I showed you during Atlanta camp.

Basically it has 2 logical additions on top of my patch #4 which barely exposed the issue via tests.

The 1st chunk is bugfix itself - as I was trying to explain it in Atlanta, we try to 'map' whatever user's permissions and access rules from webforms onto SQL language so the database returns exactly what user is allowed to view.

The 2nd part is the thing about caching - views by default uses cache-tag based caching algorithm. And the views result would get cached and wouldn't be refreshed if you change access rules of a webform. Just a few lines of code in ::doPostSave() take care of that - they double check if the webform that has just been saved has any changes in access rules; in case it does, it resets the webform_submission_list cache tag which is basically responsible for any listing of webform submission entities, views being among such listing. That way we manage to reset views cache and do it in a relatively gentle way.

I attach the interdiff and the overall patch to this comment.

Status: Needs review » Needs work

The last submitted patch, 6: 3006765-entity-access-in-view-query-6.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bucefal91’s picture

Status: Needs work » Needs review
FileSize
6.38 KB
27.62 KB

I messed up a little bit my git local repo when uploading my last patches. Let me upload the correct set of files.

Status: Needs review » Needs work

The last submitted patch, 8: 3006765-entity-access-in-view-query-6.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bucefal91’s picture

Status: Needs work » Needs review
FileSize
28.08 KB
469 bytes

Let's see if this addition solves the test failures. It should solve at least part of them. \Drupal::entityQuery() enforces access check by default for the current user, so I had to adjust the simpletest trait to explicitly tell it not to enforce access check since that ::getLastSubmissionId() is being used only from within test code and thus should not be access restricted.

Status: Needs review » Needs work

The last submitted patch, 10: 3006765-entity-access-in-view-query-10.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bucefal91’s picture

Status: Needs work » Needs review
FileSize
3.28 KB
31.17 KB

Let's see if this one takes us any further. There are actually a lot of places where \Drupal::entityQuery() is being used and by default this guy enforces access checks. In some spots these access checks make sense, in others they do not. For example, at the moment of checking submission limits we do not want to use the access checks because even if the user does not have access to the submission, the submission itself should still count towards the limit.

Status: Needs review » Needs work

The last submitted patch, 12: 3006765-entity-access-in-view-query-12.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bucefal91’s picture

Status: Needs work » Needs review
FileSize
33.61 KB
2.82 KB

Alright, here's a next iteration. A few additional spots where ->accessCheck(FALSE) was required on entity query. Additionally, from the failing tests it became obvious I should not enforce access check if the user has "administer webform" permission.

At this point I hope to see only a couple of failing tests.

Status: Needs review » Needs work

The last submitted patch, 14: 3006765-entity-access-in-view-query-14.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

bucefal91’s picture

Status: Needs work » Needs review
FileSize
633 bytes
34.23 KB

Ahm... let's see if I am lucky :)

bucefal91’s picture

Yes, I am!!! :)

Jacob, all the spot lights are on you now :) Basically I have implemented the access restrictions on SQL level. Views and \Drupal::entityQuery() automatically pick it up. In case you do not want the result of entity query to contain stuff exclusively visible to the currently logged in user, then just invoke ->accessCheck(FALSE) on the query object to disable it.

Rest of the patch is tailoring the pre-existing test cases. Many of them had somewhat relaxed their setup, i.e. some lacked a permission on the dummy user or things of that kind and so far it wasn't a problem because there was no access enforcement on SQL level. But as it kicked in, all of these minor aspects came afloat on the surface.

What do you say? Ain't it look beautiful? :)

jrockowitz’s picture

The patch looks marvelous, especially because all the tests are passing. The webform_query_webform_submission_access_alter() looks really understandable. I will review the patch.

jrockowitz’s picture

Priority: Normal » Critical

I am not sure we need WebformAccessRulesManager::queryTagWebformSubmissionAccessAlter. Maybe all this logic could be moved in to webform_query_webform_submission_access_alter()

I having a hard time following WebformSubmissionViewsAccessRulesTest and WebformSubmissionViewsPermissionsTest. There is a lot going on in the tests and maybe the code's setup and flow need to be better documented.

BTW, I would like to make these tweaks and some minor code cleanup. I could use more experience writing PHPUnit tests.

I am marking this as critical because it is a stable release blocker.

bucefal91’s picture

Hi!

I introduced WebformAccessRulesManager::queryTagWebformSubmissionAccessAlter() with the idea that 'everything related to interpreting access rules belongs to WebformAccessRulesManager class'. I do not insist that we keep this method around. I can incorporate most of it into the webform_query_webform_submission_access_alter(). Let me see if I can attach an alternative patch where I'll try to get this done.

About the 2 tests: hehe, I actually thought they would appear straight forward to you :) Let me try to explain them.. They both function pretty much along the same line: we are testing 3 difference scenarios:

  1. User without any access to submissions
  2. User with 'view own' access to submissions
  3. User with 'view any' access to submissions

The difference between the 2 tests is that one articulates access through user permission (WebformSubmissionViewsPermissionsTest) and another through webform access rules (WebformSubmissionViewsAccessRulesTest). Then, for each of the 3 test cases, we run the following:

  • Execute a pre-crafted view that just displays a table of submissions. We are going to test that view contains only a subset of submissions to which the currently logged in user has access to.
  • So once we've opened admin/structure/webform/test/views_access we just parse the output and figure out which submissions were visible on that page, the page produced by the view.
  • Then, for all the webform submissions in the system, check if current user actually has 'view' access to it. In case he does, assert the submission was present in the view output, otherwise assert the submission was absent in the view output.

What i was trying to test there is that view's output blindly follows the actual access logic.. i.e. view must obey whatever yields $webform_submission->access('view'). By 'obey' I mean either showing or hiding the row.

jrockowitz’s picture

@bucefal91 Thanks for the explanation. Are you okay if I create the next patch and make some minor tweaks to the tests.

Let's hold off from doing anything to WebformAccessRulesManager::queryTagWebformSubmissionAccessAlter().

bucefal91’s picture

Yes, sure. Do take the lead :)

jrockowitz’s picture

My changes to webform_query_webform_submission_access_alter*() include…

  • Using ?: operator to set $op and $account
  • Move any and own permission logic into if/then code block.
  • Adding more comments

Changes to tests…

  • Removed webform_test_submission_views.module and moved the new view to webform_test_views.module
  • Use the default 'contact' webform for tests
  • Add API comments to all variables
  • Whenever possible use single quotes.
  • Consolidate tests into one test class
  • Allowing for some duplicate code to improve readability.

Some personal notes

Your code and approach to solving this challenging problem and even your tests are really smart. Still, try to make sure to keep it simple.
For the tests, it is okay to have some duplicate code. Once there are more than two instances of any code, it is worth refactoring and removing the duplication.

Next steps

Remove WebformAccessRulesManager::queryTagWebformSubmissionAccessAlter

jrockowitz’s picture

jrockowitz’s picture

So I ran into a major issue with cache invalidation because adding cache tags to the renderer via webform_query_webform_submission_access_alter() does account for the actual View being cached and needing it's actual cache tags updated.

Conceptually, it started to get real tricky deciding how to add cache tags to the executed View based on webform_query_webform_submission_access_alter() so I decided to take a different approach and rely on WebformSubmission's list_cache_tags.

By adding "config:webform_list" to the WebformSubmission's list_cache_tags it makes sure anytime a webform is saved any list of submission is updated. For changes to users and permissions (aka roles) I had no choice but to add _webform_clear_webform_submission_list_cache_tag($entity);

I also adding caching for webform access rules to prevent having to load every webform for each query.

@bucefal91 Maybe we can figure out how to add the 'user' cache context to all webform submission views.

bucefal91’s picture

Fiufff.... I wanna go to sleep soon, so I'll just post what I got so far. It is all superficial:

  • After your refactoring WebformAccessRulesManager no longer needs entity_type.manager service to be injected into it.
  • In the same class, since there are a couple of methods you exposed as 'public', the @inheritdoc comment will suffice on them since now they are documented on the interface level.
  • In the test class, I corrected the assert message from "has incorrect access" to "has correct access" :)
  • In the same test class, I renamed "anonymous_user" to "without_access" because that's what it technically is. That user is a logged in one, just without any access granted.

A few points I will investigate tomorrow and will post additionally:

  • In the test class, the $check_access argument of ::checkUserSubmissionAccess() looks worrying. I see the test fails without it. But I'd expect the test to pass both with granted and revoked permissions. After all, it's not a rocket science that we are doing there - just making sure $webform_submission->access('view') is replicated onto the view result. I would like to investigate why the test fails without the $check_access tomorrow.
  • That's a lot of action there about the cache invalidation. Views design indeed looks a little short in terms of cache tags and cache contexts. I have found a couple of clues on how to append 'user' context into the view but I need some time to confirm the result looks prettier than the current code :) I also favor adding 'user' context instead of doing all this stuff.
jrockowitz’s picture

Thanks for reviewing the patch. All your changes make sense.

In the test class, the $check_access argument of ::checkUserSubmissionAccess() looks worrying.

I did this because the WebformSubmission::access() results are being cached within the actual tests. I did not see any immediate way to clear them without probably clearing the Views cache. The purpose of the second ::checkUserSubmissionAccess() call is to make sure no submissions are exposed.

That's a lot of action there about the cache invalidation.

I felt it was safer to be sure that the webform submission cache was cleared verse accidentally exposing data to the wrong user. We might only need to clear the 'webform_submission_list' cache tag when a role is updated since almost everything in Drupal is cached (and cleared) per user.

bucefal91’s picture

Yep. I understand you about the $check_access. It just smells to me like a possible crack where at some point a bug might sneak through. Through error&trial I discovered we had to reset static cache of webform submission storage and access handlers. I am attaching an interdiff that simplifies away this $check_access parameter and still yields all assertions positive.

As a next step I will now see if we can inject 'user' context into the view's caching so we can simplify the cache invalidation procedures.

jrockowitz’s picture

Are you sure that resetting the cache does not invalidate the second test which is to confirm that when a permission or access rule is changed the submission view's cache is reset.

bucefal91’s picture

To the best of my knowledge, we are good there. If we go to examine the source code of both ::resetCache() methods, they turn out to be pretty simple.

Access control handler:

  public function resetCache() {
    $this->accessCache = [];
  }

Really not much, just resetting an in-memory cache for this script run.

Storage handler:

  public function resetCache(array $ids = NULL) {
    if ($ids) {
      parent::resetCache($ids);
      if ($this->entityType->isPersistentlyCacheable()) {
        $cids = [];
        foreach ($ids as $id) {
          unset($this->latestRevisionIds[$id]);
          $cids[] = $this->buildCacheId($id);
        }
        $this->cacheBackend->deleteMultiple($cids);
      }
    }
    else {
      parent::resetCache();
      if ($this->entityType->isPersistentlyCacheable()) {
        Cache::invalidateTags([$this->entityTypeId . '_values']);
      }
      $this->latestRevisionIds = [];
    }
  }

and the parent::resetCache() from above ends up being:

  public function resetCache(array $ids = NULL) {
    if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
      foreach ($ids as $id) {
        $this->memoryCache->delete($this->buildCacheId($id));
      }
    }
    else {
      // Call the backend method directly.
      $this->memoryCache->invalidateTags([$this->memoryCacheTag]);
    }
  }

A little more of action, but still quite limited - it resets in-memory cache ($this->memoryCache) if it's statically cacheable and in case it is also cacheable in some kind of persistent storage, reset there too ($this->cacheBackend). I do not see that we could affect views caching with these 2 methods.

bucefal91’s picture

Alternative way around it would be to check whether the user is supposed to access the webform submission through an HTTP request in the test browser. Since we'd be using the underlying test browser, it will be an independent script run and we will not be facing issues of static caching.
I just replaced locally this

      foreach ($webform_submissions as $webform_submission) {
        if ($webform_submission->access('view', $account)) {
          $expected_sids[] = $webform_submission->id();
        }
      }

with

      foreach ($webform_submissions as $webform_submission) {
        $this->drupalGet($webform_submission->toUrl());
        if ($this->getSession()->getStatusCode() == 200) {
          $expected_sids[] = $webform_submission->id();
        }
      }

So we basically try to open the webform submission with currently logged in user and take it as a 'yes' or 'no' on whether user is expected to see that submission in the view results.

This alternative approach gives me green light in local tests. I personally do not have preference between these 2 implementations. Though in the 2nd implementation it's bulletproof we are not messing with views caching.

jrockowitz’s picture

Your explanation from #30 is good enough for me.

bucefal91’s picture

I've given it something like 3 hours now... No positive result. By now I come to conclusion the code you have written is the best we can do about invalidating views cache.

There is views core issue which approaches this problem: #2811041: Allow views base tables to define additional cache tags and max age, but it's not committed yet. I personally deem the last patch OK.

jrockowitz’s picture

Status: Needs review » Reviewed & tested by the community

Yep, and it took me 3 hours to come up with that solution.

  • bucefal91 committed a53d6a4 on 8.x-5.x
    Issue #3006765 by jrockowitz, bucefal91: Enforcing appropriate access...
bucefal91’s picture

Status: Reviewed & tested by the community » Fixed

Committed :)

  • jrockowitz committed 5581986 on 8.x-5.x authored by bucefal91
    Issue #3006765 by bucefal91, jrockowitz, sorin_cocorada: Webform...
othermachines’s picture

This looks like a good and necessary fix, but I'm wondering if there should be a change record.

I'm not 100% sure the fix in this issue is my culprit, but it seems likely. I'm extending a webform with some custom code that relies on submission data and after updating to 8.x-5.0-rc27 from -rc21 my code stopped working for anon users. Adding accessCheck(FALSE) to my query was simple and did the trick, but it took me a little while to track it down.

Thanks for your great work. Cheers -

jrockowitz’s picture

Here is the change record.

othermachines’s picture

Thanks very much.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.