CVE-2020-26212: Missing Authorization (CWE-862)
value: GLPI stands for Gestionnaire Libre de Parc Informatique and it is a Free Asset and IT Management Software package, that provides ITIL Service Desk features, licenses tracking and software auditing. In GLPI before version 9.5.3, any authenticated user has read-only permissions to the planning of every other user, even admin ones. Steps to reproduce the behavior: 1. Create a new planning with 'eduardo.mozart' user (from 'IT' group that belongs to 'Super-admin') into it's personal planning at 'Assistance' > 'Planning'. 2. Copy the CalDAV url and use a CalDAV client (e.g. Thunderbird) to sync the planning with the provided URL. 3. Inform the username and password from any valid user (e.g. 'camila' from 'Proativa' group). 4. 'Camila' has read-only access to 'eduardo.mozart' personal planning. The same behavior happens to any group. E.g. 'Camila' has access to 'IT' group planning, even if she doesn't belong to this group and has a 'Self-service' profile permission). This issue is fixed in version 9.5.3. As a workaround, one can remove the `caldav.php` file to block access to CalDAV server.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 6.5
Published: 2020-11-25T17:15Z
Repository: https://github.com/glpi-project/glpi
Language: PHP
Files changed: inc/caldav/backend/principal.class.php, inc/caldav/plugin/acl.class.php, inc/caldav/traits/caldavprincipalstrait.class.php, tests/functionnal/Glpi/CalDAV/Server.php
Commit date: 2020-11-25 09:18:56 +0100
Commit message: Merge pull request from GHSA-qmw3-87hr-5wgx


Diff: diff --git a/inc/caldav/backend/principal.class.php b/inc/caldav/backend/principal.class.php
index 2e3b550347f..5e88bffa3f2 100644
--- a/inc/caldav/backend/principal.class.php
+++ b/inc/caldav/backend/principal.class.php
@@ -35,10 +35,11 @@ namespace Glpi\CalDAV\Backend;
 if (!defined('GLPI_ROOT')) {
    die("Sorry. You can't access this file directly");
 }
 
 use Glpi\CalDAV\Node\Property;
+use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
 use Glpi\CalDAV\Traits\CalDAVUriUtilTrait;
 use Sabre\DAV\PropPatch;
 use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
 
 /**
@@ -48,68 +49,30 @@ use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
  *
  * @since 9.5.0
  */
 class Principal extends AbstractBackend {
 
+   use CalDAVPrincipalsTrait;
    use CalDAVUriUtilTrait;
 
    const PRINCIPALS_ROOT = 'principals';
    const PREFIX_GROUPS   = self::PRINCIPALS_ROOT . '/groups';
    const PREFIX_USERS    = self::PRINCIPALS_ROOT . '/users';
 
    public function getPrincipalsByPrefix($prefixPath) {
 
-      global $DB;
-
       $principals = [];
 
       switch ($prefixPath) {
          case self::PREFIX_GROUPS:
-            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)
-                && empty($_SESSION['glpigroups'])) {
-               // User cannot read planning of everyone and has no groups.
-               break;
-            }
-
-            $groups_criteria = getEntitiesRestrictCriteria(
-               \Group::getTable(),
-               'entities_id',
-               $_SESSION['glpiactiveentities'],
-               true
-            );
-
-            // Limit to groups visible in planning (see Planning::showAddGroupForm())
-            $groups_criteria['is_task'] = 1;
-
-            // Limit to users groups if user cannot read planning of everyone
-            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
-               $groups_criteria['id'] = $_SESSION['glpigroups'];
-            }
-
-            $groups_iterator = $DB->request(
-               [
-                  'FROM'  => \Group::getTable(),
-                  'WHERE' => $groups_criteria,
-               ]
-            );
+            $groups_iterator = $this->getVisibleGroupsIterator();
             foreach ($groups_iterator as $group_fields) {
                $principals[] = $this->getPrincipalFromGroupFields($group_fields);
             }
             break;
          case self::PREFIX_USERS:
-            if (!\Session::haveRightsOr(\Planning::$rightname, [\Planning::READALL, \Planning::READGROUP])) {
-               // Can see only personnal planning
-               $rights = 'id';
-            } else if (\Session::haveRight(\Planning::$rightname, \Planning::READGROUP)
-                && !\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
-               // Can see only planning from users sharing same groups
-               $rights = 'groups';
-            } else {
-               // Can see planning from users having rights on planning elements
-               $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
-            }
-            $users_iterator = \User::getSqlSearchResult(false, $rights);
+            $users_iterator = $this->getVisibleUsersIterator();
             foreach ($users_iterator as $user_fields) {
                $principals[] = $this->getPrincipalFromUserFields($user_fields);
             }
             break;
       }
diff --git a/inc/caldav/plugin/acl.class.php b/inc/caldav/plugin/acl.class.php
index 10e3ee34629..d2d61a68f05 100644
--- a/inc/caldav/plugin/acl.class.php
+++ b/inc/caldav/plugin/acl.class.php
@@ -35,22 +35,26 @@ namespace Glpi\CalDAV\Plugin;
 if (!defined('GLPI_ROOT')) {
    die("Sorry. You can't access this file directly");
 }
 
 use Glpi\CalDAV\Backend\Principal;
+use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
 use Glpi\CalDAV\Traits\CalDAVUriUtilTrait;
 use Sabre\CalDAV\Calendar;
 use Sabre\CalDAV\CalendarObject;
+use Sabre\DAVACL\IACL;
 use Sabre\DAVACL\Plugin;
+use Session;
 
 /**
  * ACL plugin for CalDAV server.
  *
  * @since 9.5.0
  */
 class Acl extends Plugin {
 
+   use CalDAVPrincipalsTrait;
    use CalDAVUriUtilTrait;
 
    public $principalCollectionSet = [
       Principal::PREFIX_GROUPS,
       Principal::PREFIX_USERS,
@@ -63,12 +67,15 @@ class Acl extends Plugin {
          $node = $this->server->tree->getNodeForPath($node);
       }
 
       $acl = parent::getAcl($node);
 
-      // Authenticated user have read access to all nodes, as node list only contains elements
-      // that user can read.
+      if (!($node instanceof IACL) || ($owner_path = $node->getOwner()) === null
+          || !$this->canViewPrincipalObjects($owner_path)) {
+         return $acl;
+      }
+
       $acl[] = [
          'principal' => '{DAV:}authenticated',
          'privilege' => '{DAV:}read',
          'protected' => true,
       ];
diff --git a/inc/caldav/traits/caldavprincipalstrait.class.php b/inc/caldav/traits/caldavprincipalstrait.class.php
new file mode 100644
index 00000000000..2c1b39d93a9
--- /dev/null
+++ b/inc/caldav/traits/caldavprincipalstrait.class.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2020 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
+ * ---------------------------------------------------------------------
+ */
+
+namespace Glpi\CalDAV\Traits;
+
+use EmptyIterator;
+use Group;
+use Iterator;
+use Planning;
+use Session;
+use User;
+
+if (!defined('GLPI_ROOT')) {
+   die("Sorry. You can't access this file directly");
+}
+
+/**
+ * Trait used for CalDAV Principals listing and visibility management.
+ *
+ * @since 9.5.3
+ */
+trait CalDAVPrincipalsTrait {
+
+   /**
+    * Check if principal objects are visible for current session.
+    *
+    * @param string $path
+    *
+    * @return bool
+    */
+   protected function canViewPrincipalObjects(string $path): bool {
+      $principal_type = $this->getPrincipalItemtypeFromUri($path);
+      switch ($principal_type) {
+         case Group::class:
+            $can_view = $this->canViewGroupObjects($this->getGroupIdFromPrincipalUri($path));
+            break;
+         case User::class:
+            $can_view = $this->canViewUserObjects($this->getUsernameFromPrincipalUri($path));
+            break;
+         default:
+            $can_view = false;
+            break;
+      }
+
+      return $can_view;
+   }
+
+   /**
+    * Check if group objects are visible for current session.
+    *
+    * @param int $group_id
+    *
+    * @return bool
+    */
+   protected function canViewGroupObjects(int $group_id): bool {
+      $groups_iterator = $this->getVisibleGroupsIterator();
+      foreach ($groups_iterator as $group) {
+         if ($group['id'] === $group_id) {
+            return true;
+         }
+      }
+
+      return false;
+   }
+
+   /**
+    * Check if user objects are visible for current session.
+    *
+    * @param string $username
+    *
+    * @return bool
+    */
+   protected function canViewUserObjects(string $username): bool {
+      $users_iterator = $this->getVisibleUsersIterator();
+      foreach ($users_iterator as $user) {
+         if ($user['name'] === $username) {
+            return true;
+         }
+      }
+
+      return false;
+   }
+
+   /**
+    * Get visible groups for current session.
+    *
+    * @return array
+    */
+   protected function getVisibleGroupsIterator(): Iterator {
+
+      global $DB;
+
+      if (!Session::haveRight(Planning::$rightname, Planning::READALL)
+          && empty($_SESSION['glpigroups'])) {
+         // User cannot read planning of everyone and has no groups.
+         return new EmptyIterator();
+      }
+
+      $groups_criteria = getEntitiesRestrictCriteria(
+         Group::getTable(),
+         'entities_id',
+         $_SESSION['glpiactiveentities'],
+         true
+      );
+
+      // Limit to groups visible in planning (see Planning::showAddGroupForm())
+      $groups_criteria['is_task'] = 1;
+
+      // Limit to users groups if user cannot read planning of everyone
+      if (!Session::haveRight(Planning::$rightname, Planning::READALL)) {
+         $groups_criteria['id'] = $_SESSION['glpigroups'];
+      }
+
+      $groups_iterator = $DB->request(
+         [
+            'FROM'  => Group::getTable(),
+            'WHERE' => $groups_criteria,
+         ]
+      );
+
+      return $groups_iterator;
+   }
+
+   /**
+    * Get visible users for current session.
+    *
+    * @return array
+    */
+   protected function getVisibleUsersIterator(): Iterator {
+
+      if (!Session::haveRightsOr(Planning::$rightname, [Planning::READALL, Planning::READGROUP])) {
+         // Can see only personnal planning
+         $rights = 'id';
+      } else if (Session::haveRight(Planning::$rightname, Planning::READGROUP)
+          && !Session::haveRight(Planning::$rightname, Planning::READALL)) {
+         // Can see only planning from users sharing same groups
+         $rights = 'groups';
+      } else {
+         // Can see planning from users having rights on planning elements
+         $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
+      }
+      return User::getSqlSearchResult(false, $rights);
+   }
+}
diff --git a/tests/functionnal/Glpi/CalDAV/Server.php b/tests/functionnal/Glpi/CalDAV/Server.php
index 1c49a9bf7c9..b456d7b4aa5 100644
--- a/tests/functionnal/Glpi/CalDAV/Server.php
+++ b/tests/functionnal/Glpi/CalDAV/Server.php
@@ -405,19 +405,18 @@ class Server extends DbTestCase {
     * Test PROPFIND method on calendar endpoints.
     * Tests validates that mandatory properties are correctly set.
     */
    public function testPropfindOnPrincipalCalendar() {
 
-      $login = TU_USER;
-      $pass  = TU_PASS;
+      $login = 'tech';
+      $pass  = 'tech';
       $user  = getItemByTypeName('User', $login);
 
-      $this->login($login, $pass);
-
       $group = new \Group();
       $group_id = (int)$group->add([
-         'name' => 'Test group'
+         'name'    => 'Test group',
+         'is_task' => 1,
       ]);
       $this->integer($group_id)->isGreaterThan(0);
       $group->getFromDB($group_id);
 
       $group_user = new \Group_User();
@@ -426,10 +425,12 @@ class Server extends DbTestCase {
             'groups_id' => $group_id,
             'users_id'  => $user->fields['id'],
          ])
       )->isGreaterThan(0);
 
+      $this->login($login, $pass);
+
       $calendars = [
          [
             'path' => 'calendars/users/' . $user->fields['name'] . '/calendar/',
             'name' => $user->getFriendlyName(),
          ],
@@ -486,10 +487,66 @@ class Server extends DbTestCase {
             (int)$xpath->evaluate('count(' . $result_path . '/d:propstat/d:prop/cal:supported-calendar-component-set/cal:comp[@name="VTODO"])')
          )->isEqualTo(1);
       }
    }
 
+   /**
+    * Test ACL on main objects.
+    */
+   public function testAcl() {
+
+      $user  = getItemByTypeName('User', 'tech');
+
+      $group = new \Group();
+      $group_id = (int)$group->add([
+         'name'    => 'Test group',
+         'is_task' => 1,
+      ]);
+      $this->integer($group_id)->isGreaterThan(0);
+      $group->getFromDB($group_id);
+
+      $group_user = new \Group_User();
+      $this->integer(
+         (int)$group_user->add([
+            'groups_id' => $group_id,
+            'users_id'  => $user->fields['id'],
+         ])
+      )->isGreaterThan(0);
+
+      $objects = [
+         'principals/users/' . $user->fields['name'],
+         'calendars/users/' . $user->fields['name'] . '/calendar/',
+         'principals/groups/' . $group_id,
+         'calendars/groups/' . $group_id . '/calendar/',
+      ];
+
+      $users_access = [
+         'normal' => 'HTTP/1.1 403 Forbidden',
+         'tech'   => 'HTTP/1.1 200 OK',
+      ];
+
+      foreach ($users_access as $username => $expected_status) {
+         $this->login($username, $username);
+
+         foreach ($objects as $path) {
+            $server = $this->getServerInstance('PROPFIND', $path);
+
+            $this->validateThatAuthenticationIsRequired($server);
+
+            $server->httpRequest->addHeader('Authorization', 'Basic ' . base64_encode($username . ':' . $username));
+
+            $response = new \Sabre\HTTP\Response();
+            $server->invokeMethod($server->httpRequest, $response, false);
+            $this->validateResponseIsOk($response, 207, 'application/xml'); // 207 'Multi-Status'
+
+            $xpath = $this->getXpathFromResponse($response);
+            $result_path = '/d:multistatus/d:response[1]';
+            $this->string($xpath->evaluate('string(' . $result_path . '/d:propstat/d:status)'))->isEqualTo($expected_status);
+         }
+      }
+   }
+
    /**
     * Test PROPFIND, GET, DELETE, PUT methods on calendar events.
     * Tests validates that mandatory properties are correctly set.
     */
    public function testMethodsCalendarEvents() {
@@ -1527,10 +1584,11 @@ VCALENDAR
    /**
     * Validate that response is OK.
     *
     * @param \Sabre\HTTP\Response $response
     * @param integer              $status
+    * @param string|null          $content_type
     */
    private function validateResponseIsOk(\Sabre\HTTP\Response $response, int $status, string $content_type) {
       $this->integer($response->getStatus())->isEqualTo($status);
       $this->string($response->getHeader('Content-Type'))->isEqualTo($content_type . '; charset=utf-8');
       $response_body = $response->getBodyAsString();

Vulnerable code:       global $DB;

            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)
                && empty($_SESSION['glpigroups'])) {
               // User cannot read planning of everyone and has no groups.
               break;
            }

            $groups_criteria = getEntitiesRestrictCriteria(
               \Group::getTable(),
               'entities_id',
               $_SESSION['glpiactiveentities'],
               true
            );

            // Limit to groups visible in planning (see Planning::showAddGroupForm())
            $groups_criteria['is_task'] = 1;

            // Limit to users groups if user cannot read planning of everyone
            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
               $groups_criteria['id'] = $_SESSION['glpigroups'];
            }

            $groups_iterator = $DB->request(
               [
                  'FROM'  => \Group::getTable(),
                  'WHERE' => $groups_criteria,
               ]
            );
            if (!\Session::haveRightsOr(\Planning::$rightname, [\Planning::READALL, \Planning::READGROUP])) {
               // Can see only personnal planning
               $rights = 'id';
            } else if (\Session::haveRight(\Planning::$rightname, \Planning::READGROUP)
                && !\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
               // Can see only planning from users sharing same groups
               $rights = 'groups';
            } else {
               // Can see planning from users having rights on planning elements
               $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
            }
            $users_iterator = \User::getSqlSearchResult(false, $rights);
      // Authenticated user have read access to all nodes, as node list only contains elements
      // that user can read.
      $login = TU_USER;
      $pass  = TU_PASS;
      $this->login($login, $pass);

         'name' => 'Test group'
Fixed code: use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
   use CalDAVPrincipalsTrait;
            $groups_iterator = $this->getVisibleGroupsIterator();
            $users_iterator = $this->getVisibleUsersIterator();
use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
use Sabre\DAVACL\IACL;
use Session;
   use CalDAVPrincipalsTrait;
      if (!($node instanceof IACL) || ($owner_path = $node->getOwner()) === null
          || !$this->canViewPrincipalObjects($owner_path)) {
         return $acl;
      }

<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2020 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\CalDAV\Traits;

use EmptyIterator;
use Group;
use Iterator;
use Planning;
use Session;
use User;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

/**
 * Trait used for CalDAV Principals listing and visibility management.
 *
 * @since 9.5.3
 */
trait CalDAVPrincipalsTrait {

   /**
    * Check if principal objects are visible for current session.
    *
    * @param string $path
    *
    * @return bool
    */
   protected function canViewPrincipalObjects(string $path): bool {
      $principal_type = $this->getPrincipalItemtypeFromUri($path);
      switch ($principal_type) {
         case Group::class:
            $can_view = $this->canViewGroupObjects($this->getGroupIdFromPrincipalUri($path));
            break;
         case User::class:
            $can_view = $this->canViewUserObjects($this->getUsernameFromPrincipalUri($path));
            break;
         default:
            $can_view = false;
            break;
      }

      return $can_view;
   }

   /**
    * Check if group objects are visible for current session.
    *
    * @param int $group_id
    *
    * @return bool
    */
   protected function canViewGroupObjects(int $group_id): bool {
      $groups_iterator = $this->getVisibleGroupsIterator();
      foreach ($groups_iterator as $group) {
         if ($group['id'] === $group_id) {
            return true;
         }
      }

      return false;
   }

   /**
    * Check if user objects are visible for current session.
    *
    * @param string $username
    *
    * @return bool
    */
   protected function canViewUserObjects(string $username): bool {
      $users_iterator = $this->getVisibleUsersIterator();
      foreach ($users_iterator as $user) {
         if ($user['name'] === $username) {
            return true;
         }
      }

      return false;
   }

   /**
    * Get visible groups for current session.
    *
    * @return array
    */
   protected function getVisibleGroupsIterator(): Iterator {

      global $DB;

      if (!Session::haveRight(Planning::$rightname, Planning::READALL)
          && empty($_SESSION['glpigroups'])) {
         // User cannot read planning of everyone and has no groups.
         return new EmptyIterator();
      }

      $groups_criteria = getEntitiesRestrictCriteria(
         Group::getTable(),
         'entities_id',
         $_SESSION['glpiactiveentities'],
         true
      );

      // Limit to groups visible in planning (see Planning::showAddGroupForm())
      $groups_criteria['is_task'] = 1;

      // Limit to users groups if user cannot read planning of everyone
      if (!Session::haveRight(Planning::$rightname, Planning::READALL)) {
         $groups_criteria['id'] = $_SESSION['glpigroups'];
      }

      $groups_iterator = $DB->request(
         [
            'FROM'  => Group::getTable(),
            'WHERE' => $groups_criteria,
         ]
      );

      return $groups_iterator;
   }

   /**
    * Get visible users for current session.
    *
    * @return array
    */
   protected function getVisibleUsersIterator(): Iterator {

      if (!Session::haveRightsOr(Planning::$rightname, [Planning::READALL, Planning::READGROUP])) {
         // Can see only personnal planning
         $rights = 'id';
      } else if (Session::haveRight(Planning::$rightname, Planning::READGROUP)
          && !Session::haveRight(Planning::$rightname, Planning::READALL)) {
         // Can see only planning from users sharing same groups
         $rights = 'groups';
      } else {
         // Can see planning from users having rights on planning elements
         $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
      }
      return User::getSqlSearchResult(false, $rights);
   }
}
      $login = 'tech';
      $pass  = 'tech';
         'name'    => 'Test group',
         'is_task' => 1,
      $this->login($login, $pass);

   /**
    * Test ACL on main objects.
    */
   public function testAcl() {

      $user  = getItemByTypeName('User', 'tech');

      $group = new \Group();
      $group_id = (int)$group->add([
         'name'    => 'Test group',
         'is_task' => 1,
      ]);
      $this->integer($group_id)->isGreaterThan(0);
      $group->getFromDB($group_id);

      $group_user = new \Group_User();
      $this->integer(
         (int)$group_user->add([
            'groups_id' => $group_id,
            'users_id'  => $user->fields['id'],
         ])
      )->isGreaterThan(0);

      $objects = [
         'principals/users/' . $user->fields['name'],
         'calendars/users/' . $user->fields['name'] . '/calendar/',
         'principals/groups/' . $group_id,
         'calendars/groups/' . $group_id . '/calendar/',
      ];

      $users_access = [
         'normal' => 'HTTP/1.1 403 Forbidden',
         'tech'   => 'HTTP/1.1 200 OK',
      ];

      foreach ($users_access as $username => $expected_status) {
         $this->login($username, $username);

         foreach ($objects as $path) {
            $server = $this->getServerInstance('PROPFIND', $path);

            $this->validateThatAuthenticationIsRequired($server);

            $server->httpRequest->addHeader('Authorization', 'Basic ' . base64_encode($username . ':' . $username));

            $response = new \Sabre\HTTP\Response();
            $server->invokeMethod($server->httpRequest, $response, false);
            $this->validateResponseIsOk($response, 207, 'application/xml'); // 207 'Multi-Status'

            $xpath = $this->getXpathFromResponse($response);
            $result_path = '/d:multistatus/d:response[1]';
            $this->string($xpath->evaluate('string(' . $result_path . '/d:propstat/d:status)'))->isEqualTo($expected_status);
         }
      }
   }

    * @param string|null          $content_type