From df93be7bddf139ef83e09111f1e23f5024117cff Mon Sep 17 00:00:00 2001
From: Thomas Steur <thomas.steur@gmail.com>
Date: Wed, 29 Apr 2015 22:24:30 +0000
Subject: [PATCH] Restore auth after API request

---
 core/API/Request.php                          |  93 ++++++++---
 core/Access.php                               |   3 +
 core/Tracker/Request.php                      |   2 +-
 plugins/API/tests/Integration/APITest.php     |  94 +++++++++++
 plugins/Login/Login.php                       |   2 -
 .../Framework/TestCase/SystemTestCase.php     |   4 +-
 tests/PHPUnit/Integration/API/RequestTest.php | 146 ++++++++++++++++++
 tests/PHPUnit/Integration/AccessTest.php      |  26 ++++
 8 files changed, 346 insertions(+), 24 deletions(-)
 create mode 100644 plugins/API/tests/Integration/APITest.php
 create mode 100644 tests/PHPUnit/Integration/API/RequestTest.php

diff --git a/core/API/Request.php b/core/API/Request.php
index 04d055f859..561e2222e3 100644
--- a/core/API/Request.php
+++ b/core/API/Request.php
@@ -213,32 +213,61 @@ class Request
         $corsHandler = new CORSHandler();
         $corsHandler->handle();
 
+        $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $this->request);
+        $shouldReloadAuth = false;
+
         try {
             // read parameters
             $moduleMethod = Common::getRequestVar('method', null, 'string', $this->request);
 
             list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
-
             list($module, $method) = self::getRenamedModuleAndAction($module, $method);
             
             PluginManager::getInstance()->checkIsPluginActivated($module);
 
             $apiClassName = self::getClassNameAPI($module);
 
-            self::reloadAuthUsingTokenAuth($this->request);
+            if ($shouldReloadAuth = self::shouldReloadAuthUsingTokenAuth($this->request)) {
+                $access = Access::getInstance();
+                $tokenAuthToRestore = $access->getTokenAuth();
+                $hadSuperUserAccess = $access->hasSuperUserAccess();
+                self::doReloadAuthUsingTokenAuth($tokenAuth);
+            }
 
             // call the method
             $returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request);
 
             $toReturn = $response->getResponse($returnedValue, $module, $method);
+
         } catch (Exception $e) {
             Log::debug($e);
 
             $toReturn = $response->getResponseException($e);
         }
+
+        if ($shouldReloadAuth) {
+            $this->restoreAuthUsingTokenAuth($tokenAuthToRestore, $hadSuperUserAccess);
+        }
+
         return $toReturn;
     }
 
+    private function restoreAuthUsingTokenAuth($tokenToRestore, $hadSuperUserAccess)
+    {
+        // if we would not make sure to unset super user access, the tokenAuth would be not authenticated and any
+        // token would just keep super user access (eg if the token that was reloaded before had super user access)
+        Access::getInstance()->setSuperUserAccess(false);
+
+        // we need to restore by reloading the tokenAuth as some permissions could have been removed in the API
+        // request etc. Otherwise we could just store a clone of Access::getInstance() and restore here
+        self::doReloadAuthUsingTokenAuth($tokenToRestore);
+
+        if ($hadSuperUserAccess && !Access::getInstance()->hasSuperUserAccess()) {
+            // we are in context of `doAsSuperUser()` and need to restore this behaviour
+            Access::getInstance()->setSuperUserAccess(true);
+        }
+    }
+
     /**
      * Returns the name of a plugin's API class by plugin name.
      *
@@ -263,26 +292,52 @@ class Request
     {
         // if a token_auth is specified in the API request, we load the right permissions
         $token_auth = Common::getRequestVar('token_auth', '', 'string', $request);
-        $access = Access::getInstance();
-
-        if ($token_auth && $token_auth !== $access->getTokenAuth()) {
-
-            /**
-             * Triggered when authenticating an API request, but only if the **token_auth**
-             * query parameter is found in the request.
-             *
-             * Plugins that provide authentication capabilities should subscribe to this event
-             * and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`)
-             * is setup to use `$token_auth` when its `authenticate()` method is executed.
-             *
-             * @param string $token_auth The value of the **token_auth** query parameter.
-             */
-            Piwik::postEvent('API.Request.authenticate', array($token_auth));
-            Access::getInstance()->reloadAccess();
-            SettingsServer::raiseMemoryLimitIfNecessary();
+
+        if (self::shouldReloadAuthUsingTokenAuth($request)) {
+            self::doReloadAuthUsingTokenAuth($token_auth);
         }
     }
 
+    /**
+     * The current session will be authenticated using this token_auth.
+     * It will overwrite the previous Auth object.
+     *
+     * @param string $tokenAuth
+     * @return void
+     */
+    private static function doReloadAuthUsingTokenAuth($tokenAuth)
+    {
+        /**
+         * Triggered when authenticating an API request, but only if the **token_auth**
+         * query parameter is found in the request.
+         *
+         * Plugins that provide authentication capabilities should subscribe to this event
+         * and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`)
+         * is setup to use `$token_auth` when its `authenticate()` method is executed.
+         *
+         * @param string $token_auth The value of the **token_auth** query parameter.
+         */
+        Piwik::postEvent('API.Request.authenticate', array($tokenAuth));
+        Access::getInstance()->reloadAccess();
+        SettingsServer::raiseMemoryLimitIfNecessary();
+    }
+
+    private static function shouldReloadAuthUsingTokenAuth($request)
+    {
+        if (!isset($request['token_auth'])) {
+            // no token is given so we just keep the current loaded user
+            return false;
+        }
+
+        // a token is specified, we need to reload auth in case it is different than the current one, even if it is empty
+        $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $request);
+
+        // not using !== is on purpose as getTokenAuth() might return null whereas $tokenAuth is '' . In this case
+        // we do not need to reload.
+
+        return $tokenAuth != Access::getInstance()->getTokenAuth();
+    }
+
     /**
      * Returns array($class, $method) from the given string $class.$method
      *
diff --git a/core/Access.php b/core/Access.php
index d670623755..8a3e89ae44 100644
--- a/core/Access.php
+++ b/core/Access.php
@@ -154,6 +154,9 @@ class Access
             return true;
         }
 
+        $this->token_auth = null;
+        $this->login = null;
+
         // if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail TODO: docs + review
         if ($this->auth === null) {
             return false;
diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php
index 7194101ee3..7eed4104e0 100644
--- a/core/Tracker/Request.php
+++ b/core/Tracker/Request.php
@@ -172,7 +172,7 @@ class Request
         if (!empty($idSite) && $idSite > 0) {
             $website = Cache::getCacheWebsiteAttributes($idSite);
 
-            if (array_key_exists('admin_token_auth', $website) && in_array($tokenAuth, $website['admin_token_auth'])) {
+            if (array_key_exists('admin_token_auth', $website) && in_array((string) $tokenAuth, $website['admin_token_auth'])) {
                 return true;
             }
         }
diff --git a/plugins/API/tests/Integration/APITest.php b/plugins/API/tests/Integration/APITest.php
new file mode 100644
index 0000000000..2362bebdf2
--- /dev/null
+++ b/plugins/API/tests/Integration/APITest.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\API\tests\Integration;
+
+use Piwik\Access;
+use Piwik\API\Request;
+use Piwik\Container\StaticContainer;
+use Piwik\Piwik;
+use Piwik\Plugins\API\API;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group API
+ * @group APITest
+ * @group Plugins
+ */
+class APITest extends IntegrationTestCase
+{
+    /**
+     * @var API
+     */
+    private $api;
+
+    private $hasSuperUserAccess = false;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->api = API::getInstance();
+
+        Fixture::createSuperUser(true);
+
+        if (!Fixture::siteCreated(1)) {
+            Fixture::createWebsite('2012-01-01 00:00:00');
+        }
+
+        $this->makeSureTestRunsInContextOfAnonymousUser();
+    }
+
+    public function tearDown()
+    {
+        Access::getInstance()->hasSuperUserAccess($this->hasSuperUserAccess);
+        parent::tearDown();
+    }
+
+    public function test_getBulkRequest_IsAbleToHandleManyDifferentRequests()
+    {
+        $token = Fixture::getTokenAuth();
+        $urls = array(
+            "method%3dVisitsSummary.get%26idSite%3d1%26date%3d2015-01-26%26period%3dday",
+            "method%3dVisitsSummary.get%26token_auth%3d$token%26idSite%3d1%26date%3d2015-01-26%26period%3dday",
+            "method%3dVisitsSummary.get%26idSite%3d1%26date%3d2015-01-26%26period%3dday",
+            "method%3dVisitsSummary.get%26idSite%3d1%26token_auth%3danonymous%26date%3d2015-01-26%26period%3dday",
+        );
+        $response = $this->api->getBulkRequest($urls);
+
+        $this->assertResponseIsPermissionError($response[0]);
+        $this->assertResponseIsSuccess($response[1]);
+        $this->assertSame(0, $response[1]['nb_visits']);
+        $this->assertResponseIsPermissionError($response[2]);
+        $this->assertResponseIsPermissionError($response[3]);
+    }
+
+    private function assertResponseIsPermissionError($response)
+    {
+        $this->assertSame('error', $response['result']);
+        $this->assertStringStartsWith('General_ExceptionPrivilegeAccessWebsite', $response['message']);
+    }
+
+    private function assertResponseIsSuccess($response)
+    {
+        $this->assertArrayNotHasKey('result', $response);
+    }
+
+    private function makeSureTestRunsInContextOfAnonymousUser()
+    {
+        Piwik::postEvent('Request.initAuthenticationObject');
+
+        $access = Access::getInstance();
+        $this->hasSuperUserAccess = $access->hasSuperUserAccess();
+        $access->setSuperUserAccess(false);
+        $access->reloadAccess(StaticContainer::get('Piwik\Auth'));
+        Request::reloadAuthUsingTokenAuth(array('token_auth' => 'anonymous'));
+    }
+
+}
diff --git a/plugins/Login/Login.php b/plugins/Login/Login.php
index ccaa203112..e8e74ed37f 100644
--- a/plugins/Login/Login.php
+++ b/plugins/Login/Login.php
@@ -13,9 +13,7 @@ use Piwik\Config;
 use Piwik\Container\StaticContainer;
 use Piwik\Cookie;
 use Piwik\FrontController;
-use Piwik\Option;
 use Piwik\Piwik;
-use Piwik\Plugins\UsersManager\UsersManager;
 use Piwik\Session;
 
 /**
diff --git a/tests/PHPUnit/Framework/TestCase/SystemTestCase.php b/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
index f8944bf880..cf26fde724 100755
--- a/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
+++ b/tests/PHPUnit/Framework/TestCase/SystemTestCase.php
@@ -597,9 +597,9 @@ abstract class SystemTestCase extends PHPUnit_Framework_TestCase
         }
     }
 
-    public function assertHttpResponseText($expectedResponseCode, $url, $message = '')
+    public function assertHttpResponseText($expectedResponseText, $url, $message = '')
     {
-        self::assertThat($url, new HttpResponseText($expectedResponseCode), $message);
+        self::assertThat($url, new HttpResponseText($expectedResponseText), $message);
     }
 
     public function assertResponseCode($expectedResponseCode, $url, $message = '')
diff --git a/tests/PHPUnit/Integration/API/RequestTest.php b/tests/PHPUnit/Integration/API/RequestTest.php
new file mode 100644
index 0000000000..9394d28306
--- /dev/null
+++ b/tests/PHPUnit/Integration/API/RequestTest.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Tests\Integration\API;
+
+use Piwik\Access;
+use Piwik\API\Request;
+use Piwik\AuthResult;
+use Piwik\Container\StaticContainer;
+use Piwik\Db;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class RequestTest extends IntegrationTestCase
+{
+    /** @var \Piwik\Auth|\PHPUnit_Framework_MockObject_MockObject */
+    private $auth;
+    /** @var \Piwik\Access|\PHPUnit_Framework_MockObject_MockObject */
+    private $access;
+
+    private $userAuthToken = 'token';
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->auth   = $this->createAuthMock();
+        $this->access = $this->createAccessMock($this->auth);
+        Access::setSingletonInstance($this->access);
+    }
+
+    public function test_process_shouldNotReloadAccessIfNoTokenAuthIsGiven()
+    {
+        $this->assertAccessNotReloaded();
+
+        $request = new Request(array('method' => 'API.getPiwikVersion'));
+        $request->process();
+
+        $this->assertSameUserAsBeforeIsAuthenticated();
+    }
+
+    public function test_process_shouldNotReloadAccessIfSameAuthTokenIsAlreadyLoaded()
+    {
+        $this->assertAccessNotReloaded();
+
+        $request = new Request(array('method' => 'API.getPiwikVersion', 'token_auth' => $this->access->getTokenAuth()));
+        $request->process();
+
+        $this->assertSameUserAsBeforeIsAuthenticated();
+    }
+
+    public function test_process_shouldReloadAccessIfAuthTokenIsDifferent()
+    {
+        // make sure tokenAuth is different then set 'AnYTOkEN' token
+        $this->assertEquals('token', $this->access->getTokenAuth());
+
+        $this->assertAccessReloadedAndRestored('AnYTOkEN');
+
+        $request = new Request(array('method' => 'API.getPiwikVersion', 'token_auth' => 'AnYTOkEN'));
+        $request->process();
+
+        // make sure token auth was restored after it was loaded with AnYTOkEN
+        $this->assertSameUserAsBeforeIsAuthenticated();
+    }
+
+    public function test_process_shouldReloadAccessIfAuthTokenIsDifferentButEmpty()
+    {
+        $this->assertEquals('token', $this->access->getTokenAuth());
+        $this->assertAccessReloadedAndRestored('');
+
+        $request = new Request(array('method' => 'API.getPiwikVersion', 'token_auth' => ''));
+        $request->process();
+
+        $this->assertSameUserAsBeforeIsAuthenticated();
+    }
+
+    public function test_process_shouldKeepSuperUserPermission_IfAccessWasManuallySet()
+    {
+        $this->access->setSuperUserAccess(true);
+        $this->assertAccessReloadedAndRestored('difFenrenT');
+
+        $request = new Request(array('method' => 'API.getPiwikVersion', 'token_auth' => 'difFenrenT'));
+        $request->process();
+
+        // make sure token auth was restored after it was loaded with difFenrenT
+        $this->assertSameUserAsBeforeIsAuthenticated();
+        $this->assertTrue($this->access->hasSuperUserAccess());
+    }
+
+    private function assertSameUserAsBeforeIsAuthenticated()
+    {
+        $this->assertEquals($this->userAuthToken, $this->access->getTokenAuth());
+    }
+
+    private function assertAccessNotReloaded()
+    {
+        $this->access->expects($this->never())->method('reloadAccess');
+    }
+
+    private function assertAccessReloadedAndRestored($expectedTokenToBeReloaded)
+    {
+        $this->access->expects($this->exactly(2))->method('reloadAccess');
+
+        // verify access reloaded
+        $this->auth->expects($this->at(0))->method('setLogin')->with($this->equalTo(null));
+        $this->auth->expects($this->at(1))->method('setTokenAuth')->with($this->equalTo($expectedTokenToBeReloaded));
+        $this->auth->expects($this->at(2))->method('authenticate')->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, 'login1', $expectedTokenToBeReloaded)));
+
+        // verify access restored
+        $this->auth->expects($this->at(3))->method('setLogin')->with($this->equalTo(null));
+        $this->auth->expects($this->at(4))->method('setTokenAuth')->with($this->equalTo($tokenRestored = $this->userAuthToken));
+        $this->auth->expects($this->at(5))->method('authenticate')->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, 'login', $this->userAuthToken)));
+    }
+
+    private function createAuthMock()
+    {
+        $authMock = $this->getMock('Piwik\Plugins\Login\Auth', array('authenticate', 'setTokenAuth', 'setLogin'));
+
+        $authMock->expects($this->any())
+                 ->method('authenticate')
+                 ->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, 'login', $this->userAuthToken)));
+
+        StaticContainer::getContainer()->set('Piwik\Auth', $authMock);
+
+        return $authMock;
+    }
+
+    private function createAccessMock($auth)
+    {
+        $mock = $this->getMockBuilder('Piwik\Access')
+                     ->setMethods(array('getTokenAuth', 'reloadAccess'))
+                     ->enableProxyingToOriginalMethods()
+                     ->getMock();
+        $mock->reloadAccess($auth);
+
+        return $mock;
+    }
+
+}
diff --git a/tests/PHPUnit/Integration/AccessTest.php b/tests/PHPUnit/Integration/AccessTest.php
index a105b72874..dcb49ad5db 100644
--- a/tests/PHPUnit/Integration/AccessTest.php
+++ b/tests/PHPUnit/Integration/AccessTest.php
@@ -332,6 +332,22 @@ class AccessTest extends IntegrationTestCase
         $this->assertTrue($access->reloadAccess(null));
     }
 
+    public function testReloadAccess_ShouldResetTokenAuthAndLogin_IfAuthIsNotValid()
+    {
+        $mock = $this->createAuthMockWithAuthResult(AuthResult::SUCCESS);
+        $access = Access::getInstance();
+
+        $this->assertTrue($access->reloadAccess($mock));
+        $this->assertSame('login', $access->getLogin());
+        $this->assertSame('token', $access->getTokenAuth());
+
+        $mock = $this->createAuthMockWithAuthResult(AuthResult::FAILURE);
+
+        $this->assertFalse($access->reloadAccess($mock));
+        $this->assertNull($access->getLogin());
+        $this->assertNull($access->getTokenAuth());
+    }
+
     public function testReloadAccessWithMockedAuthValid()
     {
         $mock = $this->createPiwikAuthMockInstance();
@@ -497,4 +513,14 @@ class AccessTest extends IntegrationTestCase
         return $mock;
     }
 
+    private function createAuthMockWithAuthResult($resultCode)
+    {
+        $mock = $this->createPiwikAuthMockInstance();
+        $mock->expects($this->once())
+            ->method('authenticate')
+            ->will($this->returnValue(new AuthResult($resultCode, 'login', 'token')));
+
+        return $mock;
+    }
+
 }
-- 
GitLab