Problem/Motivation

When someone masquerading as other user, the last user entity got changes in last access time (by core's user_last_access_subscriber service)
To prevent changing user entity it needs to decorate this service

Issue we have on D7 we also have on D8.
https://www.drupal.org/project/masquerade/issues/2921097

Proposed resolution

- introduce configuration to manage behavior - masquerade.settings configuration's key update_user_last_access to TRUE and post update hook for existing sites
- decorate user_last_access_subscriber service to prevent updates
- decorate session_manager.metadata_bag service to prevent "session is started" checks, see comments #23 and #24

Remaining tasks

- review and do testing with other contrib modules
- commit

User interface changes

no

API changes

- Added \Drupal\masquerade\Session\MetadataBag with setMasquerade, getMasquerade, cleanMasquerade methods accessible from session->getMetadata() object
- Added configuration with update hoot to preserve existing behavior

Data model changes

Session flag now wrapped into own session bag with useful methods

Release notes snippet

Masqueraded user's last access time no longer updated by default

Comments

xSDx created an issue. See original summary.

xsdx’s picture

Providing patch.

xsdx’s picture

Status: Active » Needs review
Ivica Arbanas’s picture

Verified. It works!
Thanks

Ivica Arbanas’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed and tested. It works!
Thanks

andypost’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Ghanks for the patch, this addition require tests and steps to reproduce, also code needs a bot of cleanup

xsdx’s picture

@andypost I'm not sure what you mean by code cleanup. I've checked code with PHPCS Drupal standard with Drupal coder and no issues there. Code consists of minimum code for this to work.

Steps for reproduce are:
1. Masquerade as any user
2. Last access timestamp should not change

Currently it changes because core user module triggers on onKernelTerminate mathod update to this timestamp in db.
So what this patch is doing is overriding this method and updating it only in case of Masquerade as user.

As for tests I agree. We should have them

megachriz’s picture

  1. +++ b/src/MasqueradeServiceProvider.php
    @@ -0,0 +1,26 @@
    +  /**
    +   * Alter definitions.
    +   *
    +   * @inheritdoc
    +   */
    

    I would note here which definitions are altered and why.

    I'm not sure if using @inheritdoc and a describing text may be used together. I remember that I've read somewhere you should use one or the other.
    Anyway, I believe the @inheritdoc should always be defined within curly brackets:
    {@inheritdoc}

  2. +++ b/src/EventSubscriber/MasqueradeUserRequestSubscriber.php
    @@ -0,0 +1,26 @@
    +  /**
    +   * On kernel terminate trigger parent in case session variable is not set.
    +   *
    +   * @inheritdoc
    +   */
    

    Same. Not sure if {@inheritdoc} should be used here if the method already has a description.

shubham.prakash’s picture

Assigned: xsdx » Unassigned
Status: Needs work » Needs review
StatusFileSize
new1.65 KB

Since {@inheritdoc} is required, extra comment has been removed.

jedihe’s picture

Assigned: Unassigned » jedihe

Working on adding a test for this.

jedihe’s picture

Just added an assert for this, in MasqueradeTest.php.

Getting the service provider to actually register in the test proved challenging, that problem was caused by the new event subscriber not being declared in masquerade.services.yml (hint: checking core/modules/system/tests/modules/service_provider_test was very helpful); the full patch includes that. Also, using $account->getLastAccessedTime() was always returning 0, so I had to fetch the value directly from users_field_data.

Let's see if the testbot likes this...

jedihe’s picture

Assigned: jedihe » Unassigned
Issue tags: -Needs tests

Alright! the test works.

andypost’s picture

  1. +++ b/src/EventSubscriber/MasqueradeUserRequestSubscriber.php
    @@ -0,0 +1,24 @@
    +    if (!isset($_SESSION['masquerading'])) {
    

    not sure session should be red from global, there's a request object with session inside passed via event object

    PS: https://github.com/symfony/symfony/issues/10557

  2. +++ b/src/MasqueradeServiceProvider.php
    @@ -0,0 +1,24 @@
    +    $definition->setClass('Drupal\masquerade\EventSubscriber\MasqueradeUserRequestSubscriber');
    

    could use MasqueradeUserRequestSubscriber::class syntax to explicitly define usage

andypost’s picture

+++ b/src/MasqueradeServiceProvider.php
@@ -0,0 +1,25 @@
+  public function alter(ContainerBuilder $container) {
+    $definition = $container->getDefinition('user_last_access_subscriber');
+    $definition->setClass(MasqueradeUserRequestSubscriber::class);

Maybe allow to configure it?

jedihe’s picture

Sure! would a simple checkbox do it? I'm thinking something like "Prevent changing last access" with description "Prevents modifying the last access timestamp for an account, when masquerading as that user".

As for implementation, I'm not sure it's possible to read config from the service provider alter() call; if that's the case, the next idea I have is to just go ahead with the override, but check for config in MasqueradeUserRequestSubscriber itself.

andypost’s picture

Status: Needs review » Needs work
Issue tags: +ContributionWeekend2020
  1. +++ b/src/EventSubscriber/MasqueradeUserRequestSubscriber.php
    @@ -0,0 +1,26 @@
    +    if (!isset($_SESSION['masquerading'])) {
    

    should use service

  2. +++ b/src/MasqueradeServiceProvider.php
    @@ -0,0 +1,26 @@
    +    $definition->setClass('Drupal\masquerade\EventSubscriber\MasqueradeUserRequestSubscriber');
    

    should be configurable and use `MasqueradeUserRequestSubscriber::class` syntax

jedihe’s picture

@andypost: #14 already fixes #17.1 and #17.2.b. As for #17.2.a, can you please have a look at #16? I just want to be sure my approach is sound before I put more work into it :).

Thanks!

andypost’s picture

Hey! The example of it is core"s configurable language manager so it's doable at least)

On other hand I prefer to keep default behaviour and get opinions from distributions and GDPR POV(

Tl'dr opt-in vs opt-out

scott_euser’s picture

I suppose the GDPR would be a wider issue if that was a concern? Eg, an opt-in on a user profile / registration with something like "Allow administrators to masquerade as me in order to support me", but that would be out of scope of this issue.

I personally cannot think of a case where a site builder would have an issue where masquerading no longer changes the last accessed timestamp on their users.

andypost’s picture

Status: Needs work » Needs review
Issue tags: +Needs change record
StatusFileSize
new4.4 KB
new5.17 KB

Still no UI and GDPR could be a reason to change port update hook, any way needs review

andypost’s picture

+++ b/masquerade.post_update.php
@@ -0,0 +1,16 @@
+    ->set('update_user_last_access', TRUE)

this affects upgrade path, probably safer to keep previous behavior for already installed sites

andypost’s picture

Status: Needs review » Needs work
+++ b/src/EventSubscriber/MasqueradeUserRequestSubscriber.php
@@ -0,0 +1,48 @@
+    if (!$session->has('masquerading') || $force) {

Just debuged this:
- session reports that it's not started! (that's why this check in isMasquerading() method)
- this call to has() throws warning for every page because of previous

RuntimeException: Failed to start the session because headers have already been sent by "/var/www/html/web/core/modules/big_pipe/src/Render/BigPipe.php" at line 264. in Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start() (line 145 of /var/www/html/web/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php)

#0 /var/www/html/web/core/lib/Drupal/Core/Session/SessionManager.php(164): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start()
#1 /var/www/html/web/core/lib/Drupal/Core/Session/SessionManager.php(118): Drupal\Core\Session\SessionManager->startNow()
#2 /var/www/html/web/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(299): Drupal\Core\Session\SessionManager->start()
#3 /var/www/html/web/vendor/symfony/http-foundation/Session/Session.php(256): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->getBag('attributes')
#4 /var/www/html/web/vendor/symfony/http-foundation/Session/Session.php(280): Symfony\Component\HttpFoundation\Session\Session->getBag('attributes')
#5 /var/www/html/web/vendor/symfony/http-foundation/Session/Session.php(65): Symfony\Component\HttpFoundation\Session\Session->getAttributeBag()
#6 /var/www/html/web/modules/masquerade/src/EventSubscriber/MasqueradeUserRequestSubscriber.php(43): Symfony\Component\HttpFoundation\Session\Session->has('masquerading')
#7 [internal function]: Drupal\masquerade\EventSubscriber\MasqueradeUserRequestSubscriber->onKernelTerminate(Object(Symfony\Component\HttpKernel\Event\PostResponseEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
#8 /var/www/html/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(111): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\PostResponseEvent), 'kernel.terminat...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
#9 /var/www/html/web/vendor/symfony/http-kernel/HttpKernel.php(88): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.terminat...', Object(Symfony\Component\HttpKernel\Event\PostResponseEvent))
#10 /var/www/html/web/vendor/stack/builder/src/Stack/StackedHttpKernel.php(32): Symfony\Component\HttpKernel\HttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\big_pipe\Render\BigPipeResponse))
#11 /var/www/html/web/core/lib/Drupal/Core/DrupalKernel.php(672): Stack\StackedHttpKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\big_pipe\Render\BigPipeResponse))
#12 /var/www/html/web/index.php(22): Drupal\Core\DrupalKernel->terminate(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\big_pipe\Render\BigPipeResponse))
#13 {main}
andypost’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests
Parent issue: » #2982514: Masquerade 8.x-2.1 stable release plan
StatusFileSize
new6.9 KB
new10.34 KB

Looks this way it works fine, needs test for decorators

I gonna add this to next beta #2982514: Masquerade 8.x-2.1 stable release plan

andypost’s picture

Issue tags: -Needs tests
StatusFileSize
new4.24 KB
new11.36 KB

Added basic kernel test for service ovverrides and a bit of cleanup
The key should remain "masquerading" to stay compatible with previous sessions

andypost’s picture

StatusFileSize
new559 bytes
new11.35 KB

Fix typo

andypost’s picture

StatusFileSize
new1 KB
new11.59 KB

Use to load user entity to be sure it is not cached in test runner

andypost’s picture

Issue tags: -Needs change record
StatusFileSize
new386 bytes
new11.43 KB

Added change record https://www.drupal.org/node/3108941 (would be great to update it with details about decorations)

Fix CS issue in last patch

andypost’s picture

Issue summary: View changes

updated summary

claudiu.cristea’s picture

Status: Needs review » Needs work

This would be a very good fix. Few remarks:

  1. +++ b/masquerade.services.yml
    @@ -20,3 +20,15 @@ services:
    +      - [setMasquerade, ['@masquerade', '@config.factory']]
    

    setMasquerade() sounds like we set only the @masquerade service. Either create another setConfigFactory() or rename to setServices() (or injectServices()).

  2. +++ b/src/EventSubscriber/MasqueradeUserRequestSubscriber.php
    @@ -0,0 +1,57 @@
    +    $force = (bool) $this->configFactory
    ...
    +    if (!$this->masquerade->isMasquerading() || $force) {
    

    Why casting to (bool), shouldn't that work out-of-the-box with our config system?

    Also, if !$this->masquerade->isMasquerading() is TRUE we don't need to compute $force anymore. So, probably, something like:

    if ($this->masquerade->isMasquerading()) {
      $force = $this->configFactory
        ->get('masquerade.settings')
        ->get('update_user_last_access');
      if (!$force) {
        return;
      }
    }
    
    parent::onKernelTerminate($event);
    

    ...would be better.

    I would also rename $force to $update_user_last_access to be consistent with the config key.

  3. +++ b/src/Session/MetadataBag.php
    @@ -0,0 +1,49 @@
    +class MetadataBag extends CoreMetadataBag {
    

    I'm not sure I get why we need this decorator. Could you, please, create a regression test for #23 so we can prove how this decorator is solving the problem?

  4. +++ b/tests/src/Functional/MasqueradeTest.php
    @@ -43,6 +45,13 @@ class MasqueradeTest extends MasqueradeWebTestBase {
    +    $auth_user = \Drupal::entityTypeManager()
    +      ->getStorage('user')
    +      ->loadUnchanged($this->auth_user->id());
    +    $this->assertEquals($original_last_access, $auth_user->getLastAccessedTime(), 'Last access timestamp for impersonated user was not changed.');
    

    This tests the default setting (FALSE). Let's extend and test also with update_user_last_access: true

  5. +++ b/tests/src/Kernel/ServiceDecoratorsTest.php
    @@ -0,0 +1,58 @@
    +  public static $modules = [
    

    s/public/protected

  6. +++ b/tests/src/Kernel/ServiceDecoratorsTest.php
    @@ -0,0 +1,58 @@
    +    $this->assertTrue(method_exists($bag, 'setMasquerade'));
    +    $this->assertTrue(method_exists($bag, 'getMasquerade'));
    +    $this->assertTrue(method_exists($bag, 'clearMasquerade'));
    

    Could we replace the 3 lines with: $this->assertInstanceOf()?

  7. +++ b/tests/src/Kernel/ServiceDecoratorsTest.php
    @@ -0,0 +1,58 @@
    +    $this->assertIdentical($bag->getMasquerade(), $uid);
    

    s/assertIdentical/assertSame

  8. +++ b/tests/src/Kernel/ServiceDecoratorsTest.php
    @@ -0,0 +1,58 @@
    +    $this->assertTrue(method_exists($service, 'setMasquerade'));
    

    Same, I think assertInstanceOf() is more appropriate.

andypost’s picture

The core issue may help to move override flag

andypost’s picture

+++ b/src/Session/MetadataBag.php
@@ -0,0 +1,49 @@
+class MetadataBag extends CoreMetadataBag {
...
+  const MASQUERADE = 'masquerading';

needs polishing after https://www.drupal.org/node/3109877 and may require 8.9 core next release

eli-t’s picture

Status: Needs work » Needs review
StatusFileSize
new11.48 KB

Rerolled patch in #28 against current HEAD of 8.x-2.x.

This intentionally has not addressed the feedback in #30 and #32 at this point.

Setting to Needs Review anyway to kick off tests.

eli-t’s picture

Running drush cr on Drupal 9.1.5 after installing the patch in #33 gives the following warning:

[warning] Declaration of Drupal\masquerade\EventSubscriber\MasqueradeUserRequestSubscriber::onKernelTerminate(Symfony\Component\HttpKernel\Event\PostResponseEvent $event) should be compatible with Drupal\user\EventSubscriber\UserRequestSubscriber::onKernelTerminate(Symfony\Component\HttpKernel\Event\TerminateEvent $event) MasqueradeUserRequestSubscriber.php:16

This also causes the test failures in https://www.drupal.org/pift-ci-job/2000260

eli-t’s picture

Status: Needs review » Needs work
eli-t’s picture

Status: Needs work » Needs review
StatusFileSize
new11.47 KB
new332 bytes

Status: Needs review » Needs work

The last submitted patch, 36: 3042321-36.patch, failed testing. View results

eli-t’s picture

The patch in #28 adds a class MasqueradeUserRequestSubscriber which extends Drupal\user\EventSubscriber\UserRequestSubscriber

This class is not covered by Drupal's BC policy: https://www.drupal.org/about/core/policies/core-change-policies/drupal-8...

In Drupal 9, the method signature for UserRequestSubscriber::onKernelTerminate() has changed, replacing the PostResponseEvent with a KernelEvent, in line with the former being deprecated since Symfony 4.3.

So as it stands I'm not sure how to make this patch compatible with both Drupal 8 and Drupal 9 without some kind of shim.

tobiberlin’s picture

As we make use of the masquerade module in order to support users of our patform we would really appreaciate this functionality to be able to see if an user really did something on our platform on his own. I did not test the patch yet - before I dig in deeper I'd like to know if there are any plans for a new release of this module which may only be compatible with Drupal 9 to solve the problem mentioned above bei ELI-T?!

jan-e’s picture

The patch in #36 just works fine for Drupal 9.4, after changing ‘true’ to ‘false’ in masquerade.settings.yml. I would not be too worried about breaking backwards compatibility with Drupal 8. And possible future BC breaks might happen, so that is something to test once in a while.

eli-t’s picture

Status: Needs work » Needs review

Agreed, now Drupal 8 is no longer supported, we no longer need a fix that works for 8 and 9.

jan-e’s picture

RTBC +1

mkalkbrenner’s picture

StatusFileSize
new11.47 KB

Patch #36 doesn't apply to the 8.x-2.x branch anymore. Here's an adjusted version without any functional changes.

w01f’s picture

Patch looks good after running db update on latest 10.0.9!

mkalkbrenner’s picture

StatusFileSize
new11.7 KB

Adjusted patch for rc3

Status: Needs review » Needs work

The last submitted patch, 45: 3042321_45.patch, failed testing. View results

jan-e’s picture

Aren't you mixing up $this->authUser and $this->auth_user in Functional/MasqueradeTest.php?
$this->auth_user is undefined in the first line of function testMasquerade().

mkalkbrenner’s picture

Might be. I didn't care about the tests and just adjusted the existing patches here to by able to be applied against the latest versions.

jan-e’s picture

StatusFileSize
new11.69 KB

Thanks for adjusting the patch for masquerade 8.x-2.0-rc3. Let us see what the attached patch does in the tests.

jan-e’s picture

Well. 2 failing tests fixed. A bit better.

mkalkbrenner’s picture

StatusFileSize
new11.98 KB

I took a quick look at the failing tests.

mkalkbrenner’s picture

Priority: Normal » Major
Status: Needs work » Needs review
jan-e’s picture

RTBC +1

mkalkbrenner’s picture

@Jan-E you can set the issue status to RTBC.

jan-e’s picture

Status: Needs review » Reviewed & tested by the community
mkalkbrenner’s picture

StatusFileSize
new11.56 KB

The last patch erroneously modified the info.yml

  • andypost committed e65dec07 on 8.x-2.x authored by mkalkbrenner
    Issue #3042321 by andypost, jedihe, mkalkbrenner, xSDx, shubham.prakash...
andypost’s picture

Status: Reviewed & tested by the community » Fixed

Thank you! Pushed to -dev version as rc4 bugfix just rolled out

andypost’s picture

Missed one coding standards message but fine

tests/src/Kernel/ServiceDecoratorsTest.php ✗ 1 more
line 11 The class short comment should describe what the class does and not simply repeat the class name

jan-e’s picture

For the record: #56 applies without problems to masquerade 8.x-2.0-rc4.

Status: Fixed » Closed (fixed)

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

solideogloria’s picture

Should a new issue be opened for adding a configuration form for updating this setting? It's not a big need for me, as I can set the config value. But others might not be able to do that.

andypost’s picture

I think it needs some readme update as it will be confusing if masquerade will update last accessed

brad.bulger’s picture

There needs to be at least an item in the README file and preferably a config form.