diff --git a/appinfo/Migrations/Version20161209151129.php b/appinfo/Migrations/Version20161209151129.php index 58393699..be20b74b 100644 --- a/appinfo/Migrations/Version20161209151129.php +++ b/appinfo/Migrations/Version20161209151129.php @@ -2,29 +2,29 @@ namespace OCA\CustomGroups\Migrations; -use Doctrine\DBAL\Migrations\AbstractMigration; +use OCP\Migration\ISchemaMigration; use Doctrine\DBAL\Schema\Schema; /** - * Auto-generated Migration: Please modify to your needs! + * Create initial tables for the customgroups app */ -class Version20161209151129 extends AbstractMigration { +class Version20161209151129 implements ISchemaMigration { /** * @param Schema $schema */ - public function up(Schema $schema) { - $this->createGroupsTable($schema); - $this->createMembersTable($schema); + public function changeSchema(Schema $schema, array $options) { + $prefix = $options['tablePrefix']; + $this->createGroupsTable($prefix, $schema); + $this->createMembersTable($prefix, $schema); } - private function createGroupsTable(Schema $schema) { - $prefix = $this->connection->getPrefix(); + private function createGroupsTable($prefix, Schema $schema) { $table = $schema->createTable("${prefix}custom_group"); - $table->addColumn('group_id', 'integer', [ + $table->addColumn('group_id', 'bigint', [ 'autoincrement' => true, 'unsigned' => true, 'notnull' => true, - 'length' => 4, + 'length' => 20, ]); $table->addColumn('uri', 'string', [ 'length' => 255, @@ -39,19 +39,18 @@ private function createGroupsTable(Schema $schema) { $table->setPrimaryKey(['group_id']); } - private function createMembersTable(Schema $schema) { - $prefix = $this->connection->getPrefix(); + private function createMembersTable($prefix, Schema $schema) { $table = $schema->createTable("${prefix}custom_group_member"); - $table->addColumn('group_id', 'integer', [ + $table->addColumn('group_id', 'bigint', [ 'unsigned' => true, 'notnull' => true, - 'length' => 4, + 'length' => 20, ]); $table->addColumn('user_id', 'string', [ 'length' => 64, 'notnull' => true, ]); - $table->addColumn('is_admin', 'integer', [ + $table->addColumn('role', 'integer', [ 'length' => 4, 'notnull' => true, 'default' => 0, diff --git a/appinfo/info.xml b/appinfo/info.xml index e0f7e00b..b27c79f2 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,6 +16,14 @@ true - + + + + OCA\CustomGroups\Dav\CustomGroupsPlugin + + + OCA\CustomGroups\Dav\RootCollection + + diff --git a/lib/Application.php b/lib/Application.php index 493af54d..9520d2d9 100644 --- a/lib/Application.php +++ b/lib/Application.php @@ -2,7 +2,7 @@ /** * @author Vincent Petry * - * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify diff --git a/lib/CustomGroupsBackend.php b/lib/CustomGroupsBackend.php index 7275cdab..41dd1569 100644 --- a/lib/CustomGroupsBackend.php +++ b/lib/CustomGroupsBackend.php @@ -2,7 +2,7 @@ /** * @author Vincent Petry * - * @copyright Copyright (c) 2016, ownCloud GmbH. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -29,10 +29,17 @@ class CustomGroupsBackend implements \OCP\GroupInterface { const GROUP_ID_PREFIX = 'customgroup_'; /** + * Custom groups handler + * * @var CustomGroupsDatabaseHandler */ private $handler; + /** + * Constructor + * + * @param CustomGroupsDatabaseHandler $handler custom groups handler + */ public function __construct( CustomGroupsDatabaseHandler $handler ) { @@ -54,7 +61,7 @@ public function implementsActions($actions) { * * @param string $uid uid of the user * @param string $gid gid of the group - * @return bool + * @return boolean true if user is in group, false otherwise */ public function inGroup($uid, $gid) { $numericGroupId = $this->extractNumericGroupId($gid); @@ -72,10 +79,10 @@ public function inGroup($uid, $gid) { * @return array an array of group names */ public function getUserGroups($uid) { - $groups = $this->handler->getUserGroups($uid); - return array_map(function($numericGroupId) { - return $this->formatGroupId($numericGroupId); - }, $groups); + $memberInfos = $this->handler->getUserMemberships($uid, null); + return array_map(function ($memberInfo) { + return $this->formatGroupId($memberInfo['group_id']); + }, $memberInfos); } /** @@ -87,11 +94,10 @@ public function getUserGroups($uid) { * @param int $limit limit or -1 to disable * @param int $offset offset * @return array an array of group names - * */ public function getGroups($search = '', $limit = -1, $offset = 0) { $groups = $this->handler->searchGroups($search, $limit, $offset); - return array_map(function($groupInfo) { + return array_map(function ($groupInfo) { return $this->formatGroupId($groupInfo['group_id']); }, $groups); } @@ -129,13 +135,13 @@ public function getGroupDetails($gid) { } /** - * Returns a list of all users in a group + * Not supported, returns an empty array. * - * @param string $gid - * @param string $search - * @param int $limit - * @param int $offset - * @return array an array of user ids + * @param string $gid group id + * @param string $search search string + * @param int $limit limit + * @param int $offset offset + * @return array empty array */ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { // not exposed to regular user management diff --git a/lib/CustomGroupsDatabaseHandler.php b/lib/CustomGroupsDatabaseHandler.php index f237b314..7a7745e7 100644 --- a/lib/CustomGroupsDatabaseHandler.php +++ b/lib/CustomGroupsDatabaseHandler.php @@ -2,7 +2,7 @@ /** * @author Vincent Petry * - * @copyright Copyright (c) 2016, ownCloud GmbH. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -29,17 +29,29 @@ */ class CustomGroupsDatabaseHandler { + const ROLE_MEMBER = 0; + const ROLE_ADMIN = 1; + /** + * Database connection + * * @var IDBConnection - */ + */ private $dbConn; /** + * Logger + * * @var ILogger */ private $logger; - + /** + * Constructor + * + * @param IDBConnection $dbConn database connection + * @param ILogger $logger logger + */ public function __construct(IDBConnection $dbConn, ILogger $logger) { $this->dbConn = $dbConn; $this->logger = $logger; @@ -50,7 +62,7 @@ public function __construct(IDBConnection $dbConn, ILogger $logger) { * * @param string $uid uid of the user * @param int $numericGroupId id of the group - * @return bool + * @return boolean true if the user is in group, false otherwise * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ public function inGroup($uid, $numericGroupId) { @@ -69,27 +81,38 @@ public function inGroup($uid, $numericGroupId) { } /** - * Get all groups a user belongs to + * Get all group memberships of the given user * * @param string $uid Name of the user - * @return array an array of numeric group ids + * @param null|int $roleFilter optional role filter + * @return array an array of member info * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ - public function getUserGroups($uid) { + public function getUserMemberships($uid, $roleFilter = null) { $qb = $this->dbConn->getQueryBuilder(); - $cursor = $qb->select('group_id') - ->from('custom_group_member') - ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid))) - ->orderBy('group_id', 'ASC') - ->execute(); + $qb->select('m.group_id', 'm.user_id', 'm.role', 'g.uri', 'g.display_name') + ->from('custom_group_member', 'm') + ->from('custom_group', 'g') + ->where($qb->expr()->eq('g.group_id', 'm.group_id')) + ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($uid))) + ->orderBy('m.group_id', 'ASC'); + + if (!is_null($roleFilter)) { + $qb->andWhere($qb->expr()->eq('role', $qb->createNamedParameter($roleFilter))); + } - $groups = []; + $cursor = $qb->execute(); + + $results = []; while ($row = $cursor->fetch()) { - $groups[] = (int)$row['group_id']; + $result = $this->formatMemberInfo($row); + $result['uri'] = $row['uri']; + $result['display_name'] = $row['display_name']; + $results[] = $result; } $cursor->closeCursor(); - return $groups; + return $results; } /** @@ -153,7 +176,8 @@ public function getGroupByUri($uri) { /** * Returns the info for a given group. * - * @param string $gid group id + * @param string $field field to filter by + * @param string $numericGroupId numeric group id * @return array|null group info or null if not found * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ @@ -186,6 +210,7 @@ public function getGroups() { /** * Creates a new group * + * @param string $uri group URI * @param string $displayName display name * @return int group id * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception @@ -197,7 +222,10 @@ public function createGroup($uri, $displayName = null) { 'display_name' => $displayName, ]); } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) { - $this->logger->logException($e, ['app' => 'customgroups', 'message' => 'Cannot create a group that already exists']); + $this->logger->logException($e, [ + 'app' => 'customgroups', + 'message' => 'Cannot create a group that already exists' + ]); return null; } @@ -233,19 +261,40 @@ public function deleteGroup($gid) { return ($result === 1); } + /** + * Update group info + * + * @param int $gid numeric group id + * @param int $uri group uri + * @param string $displayName group display name + * @return bool true if the info got updated, false otherwise + * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception + */ + public function updateGroup($gid, $uri, $displayName) { + $qb = $this->dbConn->getQueryBuilder(); + $result = $qb->update('custom_group') + ->set('uri', $qb->createNamedParameter($uri)) + ->set('display_name', $qb->createNamedParameter($displayName)) + ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) + ->execute(); + + return $result === 1; + } + /** * Add a user to a group. * * @param string $uid user id * @param int $gid numeric group id - * @return bool true if user was added, false otherwise + * @param int $role initial permission + * @return boolean true if user was added, false otherwise * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ - public function addToGroup($uid, $gid, $isAdmin = false) { + public function addToGroup($uid, $gid, $role = self::ROLE_MEMBER) { $result = $this->dbConn->insertIfNotExist('*PREFIX*custom_group_member', [ 'user_id' => $uid, 'group_id' => $gid, - 'is_admin' => $isAdmin ? 1 : 0 + 'role' => $role ]); return ($result === 1); @@ -273,16 +322,23 @@ public function removeFromGroup($uid, $gid) { * Returns the group members * * @param int $gid numeric group id + * @param null|bool $roleFilter optional role filter, set to true or false to + * filter by non-admin-only or admin-only * @return array array of member info * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ - public function getGroupMembers($gid) { + public function getGroupMembers($gid, $roleFilter = null) { $qb = $this->dbConn->getQueryBuilder(); - $cursor = $qb->select(['user_id', 'group_id', 'is_admin']) + $qb->select(['user_id', 'group_id', 'role']) ->from('custom_group_member') ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) - ->orderBy('user_id', 'ASC') - ->execute(); + ->orderBy('user_id', 'ASC'); + + if (!is_null($roleFilter)) { + $qb->andWhere($qb->expr()->eq('role', $qb->createNamedParameter($roleFilter))); + } + + $cursor = $qb->execute(); $results = []; while ($row = $cursor->fetch()) { @@ -303,7 +359,7 @@ public function getGroupMembers($gid) { */ public function getGroupMemberInfo($gid, $uid) { $qb = $this->dbConn->getQueryBuilder(); - $cursor = $qb->select(['user_id', 'group_id', 'is_admin']) + $cursor = $qb->select(['user_id', 'group_id', 'role']) ->from('custom_group_member') ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($uid))) @@ -325,14 +381,14 @@ public function getGroupMemberInfo($gid, $uid) { * * @param int $gid numeric group id * @param int $uid user id - * @param bool $isAdmin whether the member is a group admin + * @param int $role membership role * @return bool true if the info got updated, false otherwise * @throws \Doctrine\DBAL\Exception\DriverException in case of database exception */ - public function setGroupMemberInfo($gid, $uid, $isAdmin) { + public function setGroupMemberInfo($gid, $uid, $role) { $qb = $this->dbConn->getQueryBuilder(); $result = $qb->update('custom_group_member') - ->set('is_admin', $qb->createNamedParameter($isAdmin ? 1 : 0)) + ->set('role', $qb->createNamedParameter($role)) ->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid))) ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($uid))) ->execute(); @@ -346,11 +402,11 @@ public function setGroupMemberInfo($gid, $uid, $isAdmin) { * @param array $row database row * @return array formatted array */ - private function formatMemberInfo($row) { + private function formatMemberInfo(array $row) { return [ 'user_id' => $row['user_id'], - 'group_id' => $row['group_id'], - 'is_admin' => (int)$row['is_admin'] !== 0, + 'group_id' => (int)$row['group_id'], + 'role' => (int)$row['role'], ]; } } diff --git a/lib/Dav/CustomGroupsPlugin.php b/lib/Dav/CustomGroupsPlugin.php new file mode 100644 index 00000000..61f72a24 --- /dev/null +++ b/lib/Dav/CustomGroupsPlugin.php @@ -0,0 +1,195 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserSession; +use OCA\CustomGroups\Dav\RootCollection; +use OCA\CustomGroups\Dav\MembershipNode; +use OCA\CustomGroups\Dav\GroupsCollection; +use Sabre\DAV\ServerPlugin; +use OCA\CustomGroups\Dav\CustomGroupMemberNode; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Xml\Element\Response; +use Sabre\DAV\Xml\Response\MultiStatus; +/** + * Sabre plugin to handle custom groups + */ +class CustomGroupsPlugin extends ServerPlugin { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + const REPORT_NAME = '{http://owncloud.org/ns}filter-members'; + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + protected $groupsHandler; + + /** + * Sabre server + * + * @var \Sabre\DAV\Server $server + */ + private $server; + + /** + * User session + * + * @var \OCP\IUserSession + */ + protected $userSession; + + /** + * Custom groups plugin + * + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param IUserSession $userSession user session + */ + public function __construct(CustomGroupsDatabaseHandler $groupsHandler, IUserSession $userSession) { + $this->groupsHandler = $groupsHandler; + $this->userSession = $userSession; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server Sabre server + */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + + $uri = $this->server->getRequestUri(); + $uri = '/' . trim($uri) . '/'; + if (strpos($uri, '/customgroups/') === false) { + return; + } + + if ($this->userSession === null || $this->userSession->getUser() === null) { + return; + } + + $this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + $ns = '{' . self::NS_OWNCLOUD . '}'; + $this->server->resourceTypeMapping[MembershipNode::class] = $ns . 'customgroups-membership'; + $this->server->resourceTypeMapping[GroupsCollection::class] = $ns . 'customgroups-group'; + $this->server->protectedProperties[] = $ns . 'user-id'; + $this->server->protectedProperties[] = $ns . 'group-uri'; + + $this->server->on('report', [$this, 'onReport']); + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * + * @param string $uri URI + * @return array + */ + public function getSupportedReportSet($uri) { + $node = $this->server->tree->getNodeForPath($uri); + if (!$node instanceof RootCollection) { + return [self::REPORT_NAME]; + } + return []; + } + + /** + * REPORT operations to look for comments + * + * @param string $reportName report name + * @param array $report report data + * @param string $uri URI + * @return bool true if processed + * @throws BadRequest if missing properties + */ + public function onReport($reportName, $report, $uri) { + $node = $this->server->tree->getNodeForPath($uri); + if (!$node instanceof RootCollection || $reportName !== self::REPORT_NAME) { + return; + } + + $requestedProps = []; + $filterRules = []; + + $ns = '{' . self::NS_OWNCLOUD . '}'; + foreach ($report as $reportProps) { + $name = $reportProps['name']; + if ($name === $ns . 'filter-rules') { + $filterRules = $reportProps['value']; + } else if ($name === '{DAV:}prop') { + // propfind properties + foreach ($reportProps['value'] as $propVal) { + $requestedProps[] = $propVal['name']; + } + } + } + + $filterUserId = null; + $filterAdminFlag = null; + foreach ($filterRules as $filterRule) { + if ($filterRule['name'] === $ns . 'user-id') { + $filterUserId = $filterRule['value']; + } else if ($filterRule['name'] === $ns . 'role') { + $filterAdminFlag = $filterRule['value']; + } + } + + if (is_null($filterUserId)) { + // an empty filter would return all existing users which would be useless + throw new BadRequest('Missing user-id property'); + } + + $memberInfos = $this->groupsHandler->getUserMemberships($filterUserId, $filterAdminFlag); + + $responses = []; + foreach ($memberInfos as $memberInfo) { + $node = new CustomGroupMemberNode($memberInfo, $this->groupsHandler, $this->userSession); + $uri = $memberInfo['uri']; + $nodePath = $this->server->getRequestUri() . '/' . $uri . '/' . $node->getName(); + $resultSet = $node->getProperties($requestedProps); + $responses[] = new Response( + $this->server->getBaseUri() . $nodePath, + [200 => $resultSet], + 200 + ); + } + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new MultiStatus($responses) + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody($xml); + + return false; + } +} diff --git a/lib/Dav/GroupMembershipCollection.php b/lib/Dav/GroupMembershipCollection.php new file mode 100644 index 00000000..f1294f94 --- /dev/null +++ b/lib/Dav/GroupMembershipCollection.php @@ -0,0 +1,268 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\PreconditionFailed; + +/** + * Group memberships collection for a given group + */ +class GroupMembershipCollection implements \Sabre\DAV\ICollection, \Sabre\DAV\IProperties { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + const PROPERTY_DISPLAY_NAME = '{http://owncloud.org/ns}display-name'; + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * Membership helper + * + * @var MembershipHelper + */ + private $helper; + + /** + * Group information + * + * @var array + */ + private $groupInfo; + + /** + * Constructor + * + * @param array $groupInfo group info + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param MembershipHelper $helper membership helper + */ + public function __construct( + array $groupInfo, + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $this->groupsHandler = $groupsHandler; + $this->groupInfo = $groupInfo; + $this->helper = $helper; + } + + /** + * Deletes the group + * + * @throws Forbidden if no permisson to delete this group + */ + public function delete() { + $groupId = $this->groupInfo['group_id']; + if (!$this->helper->isUserAdmin($groupId)) { + throw new Forbidden("No permission to delete group \"$groupId\""); + } + $this->groupsHandler->deleteGroup($groupId); + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() { + return (string)$this->groupInfo['uri']; + } + + /** + * Not supported + * + * @param string $name the new name + * @throws MethodNotAllowed not supported + */ + public function setName($name) { + throw new MethodNotAllowed(); + } + + /** + * Returns null + * + * @return int null + */ + public function getLastModified() { + return null; + } + + /** + * Updates properties on this node. + * + * @param PropPatch $propPatch PropPatch request + */ + public function propPatch(PropPatch $propPatch) { + $propPatch->handle(self::PROPERTY_DISPLAY_NAME, [$this, 'updateDisplayName']); + } + + /** + * Returns a list of properties for this node. + * + * @param array|null $properties requested properties or null for all + * @return array property values + */ + public function getProperties($properties) { + if ($properties === null || in_array(self::PROPERTY_DISPLAY_NAME, $properties)) { + return [ + self::PROPERTY_DISPLAY_NAME => $this->groupInfo['display_name'], + ]; + } + return []; + } + + /** + * Adds a new member to this group + * + * @param string $userId user id to add + * @param resource|string $data unused + * @throws Forbidden if the current user has insufficient permissions + * @throws PreconditionFailed if the user did not exist + */ + public function createFile($userId, $data = null) { + $groupId = $this->groupInfo['group_id']; + if (!$this->helper->isUserAdmin($groupId)) { + throw new Forbidden("No permission to add members to group \"$groupId\""); + } + // check if the user name actually exists + $user = $this->helper->getUser($userId); + // not existing user or mismatch user casing + if (is_null($user) || $userId !== $user->getUID()) { + throw new PreconditionFailed("The user \"$userId\" does not exist"); + } + + if (!$this->groupsHandler->addToGroup($userId, $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER)) { + throw new PreconditionFailed("The user \"$userId\" is already member of this group"); + } + } + + /** + * Not supported + * + * @param string $name name + * @throws MethodNotAllowed not supported + */ + public function createDirectory($name) { + throw new MethodNotAllowed('Cannot create collections'); + } + + /** + * Returns a membership node + * + * @param string $userId user id + * @return CustomGroupMemberNode membership node + * @throws NotFound if the given user has no membership in this group + * @throws Forbidden if the current user has insufficient permissions + */ + public function getChild($userId) { + $groupId = $this->groupInfo['group_id']; + if (!$this->helper->isUserMember($groupId) && !$this->helper->isUserAdmin($groupId)) { + throw new Forbidden("No permission to list members of group \"$groupId\""); + } + $memberInfo = $this->groupsHandler->getGroupMemberInfo($groupId, $userId); + if (is_null($memberInfo)) { + throw new NotFound( + "User with id \"$userId\" is not member of group with uri \"$groupId\"" + ); + } + return $this->createCustomGroupMemberNode($memberInfo); + } + + /** + * Returns a list of all memberships + * + * @return CustomGroupMemberNode[] list of memberships + * @throws Forbidden if the current user has insufficient permissions + */ + public function getChildren() { + $groupId = $this->groupInfo['group_id']; + if (!$this->helper->isUserMember($groupId) + && !$this->helper->isUserAdmin($groupId)) { + throw new Forbidden("No permission to list members of group \"$groupId\""); + } + $members = $this->groupsHandler->getGroupMembers($groupId); + return array_map(function ($memberInfo) { + return $this->createCustomGroupMemberNode($memberInfo); + }, $members); + } + + /** + * Returns whether a user has a membership in this group. + * + * @param string $userId user id + * @return boolean true if the user has a membership, false otherwise + * @throws Forbidden if the current user has insufficient permissions + */ + public function childExists($userId) { + $groupId = $this->groupInfo['group_id']; + if (!$this->helper->isUserMember($groupId)) { + throw new Forbidden("No permission to list members of group \"$groupId\""); + } + return $this->groupsHandler->inGroup($userId, $groupId); + } + + /** + * Update the display name. + * Returns 403 status code if the current user has insufficient permissions. + * + * @param string $displayName display name to set + * @return boolean|int true or status code + */ + public function updateDisplayName($displayName) { + if (!$this->helper->isUserAdmin($this->groupInfo['group_id'])) { + return 403; + } + + $result = $this->groupsHandler->updateGroup( + $this->groupInfo['group_id'], + $this->groupInfo['uri'], + $displayName + ); + $this->groupInfo['display_name'] = $displayName; + + return $result; + } + + /** + * Creates a membership node based on the given membership info. + * + * @param array $memberInfo membership info + * @return CustomGroupMemberNode membership node + */ + private function createCustomGroupMemberNode(array $memberInfo) { + return new MembershipNode( + $memberInfo, + $memberInfo['user_id'], + $this->groupsHandler, + $this->helper + ); + } +} diff --git a/lib/Dav/GroupsCollection.php b/lib/Dav/GroupsCollection.php new file mode 100644 index 00000000..97a04478 --- /dev/null +++ b/lib/Dav/GroupsCollection.php @@ -0,0 +1,178 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use Sabre\DAV\ICollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\MethodNotAllowed; + +/** + * Collection of custom groups + */ +class GroupsCollection implements ICollection { + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * Membership helper + * + * @var MembershipHelper + */ + private $helper; + + /** + * Constructor + * + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param MembershipHelper $helper helper + */ + public function __construct( + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $this->groupsHandler = $groupsHandler; + $this->helper = $helper; + } + + /** + * Not supported + * + * @param string $name name + * @param resource|string $data unused + * @throws MethodNotAllowed not supported + */ + public function createFile($name, $data = null) { + throw new MethodNotAllowed('Cannot create regular nodes'); + } + + /** + * Creates a new custom group + * + * @param string $name group URI + * @throws MethodNotAllowed if the group already exists + */ + public function createDirectory($name) { + $groupId = $this->groupsHandler->createGroup($name, $name); + if (is_null($groupId)) { + throw new MethodNotAllowed("Group with uri \"$name\" already exists"); + } + + // add current user as admin + $this->groupsHandler->addToGroup($this->helper->getUserId(), $groupId, true); + } + + /** + * Returns the custom group node for the given URI. + * + * @param string $name group URI + * @return GroupMembershipCollection node + * @throws NotFound if the requested group does not exist + */ + public function getChild($name) { + $group = $this->groupsHandler->getGroupByUri($name); + if (is_null($group)) { + throw new NotFound("Group with uri \"$name\" not found"); + } + return $this->createMembershipsCollection($group); + } + + /** + * Returns nodes for all existing custom groups. + * + * @return GroupMembershipCollection[] custom group nodes + */ + public function getChildren() { + $allGroups = $this->groupsHandler->getGroups(); + return array_map(function ($groupInfo) { + return $this->createMembershipsCollection($groupInfo); + }, $allGroups); + } + + /** + * Returns whether a custom group exists. + * + * @param string $name group URI + * @return boolean true if the group exists, false otherwise + */ + public function childExists($name) { + return !is_null($this->groupsHandler->getGroupByUri($name)); + } + + /** + * Not supported + * + * @throws MethodNotAllowed not supported + */ + public function delete() { + throw new MethodNotAllowed('Cannot delete this collection'); + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string node name + */ + public function getName() { + return 'groups'; + } + + /** + * Not supported + * + * @param string $name name + * @throws MethodNotAllowed not supported + */ + public function setName($name) { + throw new MethodNotAllowed('Cannot rename this collection'); + } + + /** + * Returns null + * + * @return int null + */ + public function getLastModified() { + return null; + } + + /** + * Creates a custom group node for the given group info. + * + * @param array $groupInfo group info + * @return GroupMembershipCollection node + */ + private function createMembershipsCollection(array $groupInfo) { + return new GroupMembershipCollection( + $groupInfo, + $this->groupsHandler, + $this->helper + ); + } +} diff --git a/lib/Dav/MembershipHelper.php b/lib/Dav/MembershipHelper.php new file mode 100644 index 00000000..2b375c6c --- /dev/null +++ b/lib/Dav/MembershipHelper.php @@ -0,0 +1,177 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserSession; +use OCP\IUserManager; +use OCP\IGroupManager; + +/** + * Membership helper + * + * Provides method related to the current user's membership and admin roles. + */ +class MembershipHelper { + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * User session + * + * @var IUserSession + */ + private $userSession; + + /** + * User manager + * + * @var IUserManager + */ + private $userManager; + + /** + * Group manager + * + * @var IGroupManager + */ + private $groupsManager; + + /** + * Membership info for the currently logged in user + * + * @var array + */ + private $userMemberInfo = []; + + /** + * Membership helper + * + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param IUserSession $userSession user session + * @param IUserManager $userManager user manager + * @param IGroupManager $groupManager group manager + */ + public function __construct( + CustomGroupsDatabaseHandler $groupsHandler, + IUserSession $userSession, + IUserManager $userManager, + IGroupManager $groupManager + ) { + $this->groupsHandler = $groupsHandler; + $this->userSession = $userSession; + $this->userManager = $userManager; + $this->groupManager = $groupManager; + } + + /** + * Returns the currently logged in user id + * + * @return string user id + */ + public function getUserId() { + return $this->userSession->getUser()->getUID(); + } + + /** + * Returns the user object for a given user id + * + * @param string $userId user id + * @return IUser|null user object or null if user does not exist + */ + public function getUser($userId) { + return $this->userManager->get($userId); + } + + /** + * Returns membership information for the current user + * + * @param int $groupId group id + * @return array membership information + */ + private function getUserMemberInfo($groupId) { + if (!isset($this->userMemberInfo[$groupId])) { + $userId = $this->getUserId(); + $this->userMemberInfo[$groupId] = $this->groupsHandler->getGroupMemberInfo($groupId, $userId); + } + return $this->userMemberInfo[$groupId]; + } + + /** + * Returns whether the current user can administrate this group + * + * @param int $groupId group id + * @return boolean true if the user can administrate, false otherwise + */ + public function isUserAdmin($groupId) { + // ownCloud admin is always admin of any custom group + if ($this->isUserSuperAdmin()) { + return true; + } + $memberInfo = $this->getUserMemberInfo($groupId); + return (!is_null($memberInfo) && $memberInfo['role']); + } + + /** + * Returns whether the current user is an ownCloud admin + * + * @return boolean true if the user is an ownCloud admin, false otherwise + */ + public function isUserSuperAdmin() { + return ($this->groupManager->isAdmin($this->getUserId())); + } + + /** + * Returns whether the current user is member of this group + * + * @param int $groupId group id + * @return boolean true if the user is member, false otherwise + */ + public function isUserMember($groupId) { + $memberInfo = $this->getUserMemberInfo($groupId); + return (!is_null($memberInfo)); + } + + /** + * Returns whether the given group's member is the one and only group admin + * + * @param int $groupId group id + * @param string $userId user id of the admin to check + * @return bool true if it's the only admin, false otherwise + */ + public function isTheOnlyAdmin($groupId, $userId) { + $groupAdmins = $this->groupsHandler->getGroupMembers($groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); + if (count($groupAdmins) > 1) { + return false; + } + if ($groupAdmins[0]['user_id'] !== $userId) { + return false; + } + + return true; + } + +} diff --git a/lib/Dav/MembershipNode.php b/lib/Dav/MembershipNode.php new file mode 100644 index 00000000..ddf833be --- /dev/null +++ b/lib/Dav/MembershipNode.php @@ -0,0 +1,221 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\PreconditionFailed; + +/** + * Membership node + */ +class MembershipNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + const PROPERTY_ROLE = '{http://owncloud.org/ns}role'; + const PROPERTY_USER_ID = '{http://owncloud.org/ns}user-id'; + const PROPERTY_GROUP_URI = '{http://owncloud.org/ns}group-uri'; + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * Membership information + * + * @var array + */ + private $memberInfo; + + /** + * Membership helper + * + * @var MembershipHelper + */ + private $helper; + + /** + * Membership info for the currently logged in user + * + * @var array + */ + private $userMemberInfo; + + /** + * Node name + * + * @var string + */ + private $name; + + /** + * Constructor + * + * @param array $memberInfo membership information + * @param string $name node name (based on user id or group id) + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param MembershipHelper $helper membership helper + */ + public function __construct( + array $memberInfo, + $name, + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $this->groupsHandler = $groupsHandler; + $this->name = $name; + $this->memberInfo = $memberInfo; + $this->helper = $helper; + } + + /** + * Removes this member from the group + * + * @throws Forbidden when no permission to delete + * @throws PreconditionFailed when membership did not exist + */ + public function delete() { + $currentUserId = $this->helper->getUserId(); + $groupId = $this->memberInfo['group_id']; + // admins can remove members + // and regular members can remove themselves + if (!$this->helper->isUserAdmin($groupId) + && !($currentUserId === $this->memberInfo['user_id'] && $this->helper->isUserMember($groupId)) + ) { + throw new Forbidden("No permission to remove members from group \"$groupId\""); + } + + // can't remove the last admin + if ($this->helper->isTheOnlyAdmin($groupId, $currentUserId)) { + throw new Forbidden("Cannot remove the last admin from the group \"$groupId\""); + } + + $userId = $this->memberInfo['user_id']; + if (!$this->groupsHandler->removeFromGroup( + $userId, + $groupId + )) { + // possibly the membership was deleted concurrently + throw new PreconditionFailed("Could not remove member \"$userId\" from group \"$groupId\""); + }; + } + + /** + * Returns the node name + * + * @return string node name + */ + public function getName() { + return $this->name; + } + + /** + * Not supported + * + * @param string $name The new name + * @throws MethodNotAllowed not supported + */ + public function setName($name) { + throw new MethodNotAllowed(); + } + + /** + * Returns null + * + * @return int null + */ + public function getLastModified() { + return null; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + * + * @param PropPatch $propPatch PropPatch query + */ + public function propPatch(PropPatch $propPatch) { + $propPatch->handle(self::PROPERTY_ROLE, [$this, 'updateAdminFlag']); + } + + /** + * Returns a list of properties for this node. + * + * @param array|null $properties requested properties or null for all + * @return array property values + */ + public function getProperties($properties) { + $result = []; + if ($properties === null || in_array(self::PROPERTY_ROLE, $properties)) { + $result[self::PROPERTY_ROLE] = $this->memberInfo['role']; + } + if ($properties === null || in_array(self::PROPERTY_USER_ID, $properties)) { + $result[self::PROPERTY_USER_ID] = $this->memberInfo['user_id']; + } + if ($properties === null || in_array(self::PROPERTY_GROUP_URI, $properties)) { + $result[self::PROPERTY_GROUP_URI] = $this->memberInfo['uri']; + } + return $result; + } + + /** + * Update the admin flag. + * Returns 403 status code if the current user has insufficient permissions + * or if the only group admin is trying to remove their own permission. + * + * @param int $rolePropValue role value + * @return boolean|int true or error status code + */ + public function updateAdminFlag($rolePropValue) { + $groupId = $this->memberInfo['group_id']; + $userId = $this->memberInfo['user_id']; + // only the group admin can change permissions + if (!$this->helper->isUserAdmin($groupId)) { + return 403; + } + + // can't remove admin rights from the last admin + if ($rolePropValue !== CustomGroupsDatabaseHandler::ROLE_ADMIN && $this->helper->isTheOnlyAdmin($groupId, $userId)) { + return 403; + } + + $result = $this->groupsHandler->setGroupMemberInfo( + $groupId, + $userId, + ($rolePropValue === CustomGroupsDatabaseHandler::ROLE_ADMIN) + ); + $this->groupInfo['role'] = $rolePropValue; + + return $result; + } + +} diff --git a/lib/Dav/RootCollection.php b/lib/Dav/RootCollection.php new file mode 100644 index 00000000..3702cdb7 --- /dev/null +++ b/lib/Dav/RootCollection.php @@ -0,0 +1,54 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\SimpleCollection; + +/** + * Root collection for the custom groups and members + */ +class RootCollection extends SimpleCollection { + /** + * Constructor + * + * @param MembershipHelper $helper membership helper + */ + public function __construct( + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $children = [ + new GroupsCollection( + $groupsHandler, + $helper + ), + new UsersCollection( + $groupsHandler, + $helper + ), + ]; + parent::__construct('customgroups', $children); + } +} diff --git a/lib/Dav/UserMembershipCollection.php b/lib/Dav/UserMembershipCollection.php new file mode 100644 index 00000000..5a4d2c6c --- /dev/null +++ b/lib/Dav/UserMembershipCollection.php @@ -0,0 +1,192 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\PreconditionFailed; +use OCA\DAV\Connector\Sabre\Principal; + +/** + * Membership collection for a specific user + */ +class UserMembershipCollection extends Principal implements \Sabre\DAV\ICollection { + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * User id + * + * @var string + */ + private $userId; + + /** + * Membership helper + * + * @var MembershipHelper + */ + private $helper; + + /** + * Constructor + * + * @param string $userId user id + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param MembershipHelper $helper membership helper + */ + public function __construct( + $userId, + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $this->groupsHandler = $groupsHandler; + $this->userId = $userId; + $this->helper = $helper; + } + + /** + * Not supported + * + * @throws MethodNotAllowed not supported + */ + public function delete() { + throw new MethodNotAllowed('Not supported'); + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() { + return $this->userId; + } + + /** + * Not supported + * + * @param string $name the new name + * @throws MethodNotAllowed not supported + */ + public function setName($name) { + throw new MethodNotAllowed(); + } + + /** + * Returns null + * + * @return int null + */ + public function getLastModified() { + return null; + } + + /** + * Not supported + * + * @throws MethodNotAllowed not supported + */ + public function createFile($name, $data = null) { + throw new MethodNotAllowed('Not supported'); + } + + /** + * Not supported + * + * @param string $name name + * @throws MethodNotAllowed not supported + */ + public function createDirectory($name) { + throw new MethodNotAllowed('Cannot create collections'); + } + + /** + * Returns a group node + * + * @param string $groupUri group uri + * @return GroupCollection group node + * @throws NotFound if the user is not member of the given group + */ + public function getChild($groupUri) { + $groupInfo = $this->groupsHandler->getGroupByUri($groupUri); + if (!is_null($groupInfo)) { + $memberInfo = $this->groupsHandler->getGroupMemberInfo($groupInfo['group_id'], $this->userId); + if (!is_null($memberInfo)) { + return $this->createGroupNode($memberInfo); + } + } + + throw new NotFound("Group with uri \"$groupUri\" not found."); + } + + /** + * Returns a list of all memberships + * + * @return CustomGroupMemberNode[] list of memberships + * @throws Forbidden if the current user has insufficient permissions + */ + public function getChildren() { + $memberInfos = $this->groupsHandler->getUserMemberships($this->userId); + return array_map(function ($memberInfo) { + return $this->createGroupNode($memberInfo); + }, $memberInfos); + } + + /** + * Returns whether a user has a membership in the given. + * + * @param string $groupUri group uri + * @return boolean true if the user has a membership, false otherwise + */ + public function childExists($groupUri) { + try { + $this->getChild($groupUri); + } catch (NotFound $e) { + return false; + } + return true; + } + + /** + * Creates a membership node based on the given membership info. + * + * @param array $memberInfo membership info + * @return CustomGroupMemberNode membership node + */ + private function createGroupNode(array $memberInfo) { + return new MembershipNode( + $memberInfo, + $memberInfo['uri'], + $this->groupsHandler, + $this->helper + ); + } +} diff --git a/lib/Dav/UsersCollection.php b/lib/Dav/UsersCollection.php new file mode 100644 index 00000000..80a3fa58 --- /dev/null +++ b/lib/Dav/UsersCollection.php @@ -0,0 +1,176 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Dav; + +use Sabre\DAV\ICollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\Forbidden; + +/** + * Collection of users + */ +class UsersCollection implements ICollection { + + /** + * Custom groups handler + * + * @var CustomGroupsDatabaseHandler + */ + private $groupsHandler; + + /** + * Membership helper + * + * @var MembershipHelper + */ + private $helper; + + /** + * Constructor + * + * @param CustomGroupsDatabaseHandler $groupsHandler custom groups handler + * @param MembershipHelper $helper + */ + public function __construct( + CustomGroupsDatabaseHandler $groupsHandler, + MembershipHelper $helper + ) { + $this->groupsHandler = $groupsHandler; + $this->helper = $helper; + } + + /** + * Not supported + * + * @param string $name name + * @param resource|string $data unused + * @throws MethodNotAllowed not supported + */ + public function createFile($name, $data = null) { + throw new MethodNotAllowed('Cannot create regular nodes'); + } + + /** + * Creates a new custom group + * + * @param string $name group URI + * @throws MethodNotAllowed if the group already exists + */ + public function createDirectory($name) { + $groupId = $this->groupsHandler->createGroup($name, $name); + if (is_null($groupId)) { + throw new MethodNotAllowed("Group with uri \"$name\" already exists"); + } + + // add current user as admin + $this->groupsHandler->addToGroup($this->helper->getUserId(), $groupId, true); + } + + /** + * Returns the given user's memberships + * + * @param string $name user id + * @return CustomGroupMembershipCollection user membership collection + * @throws Forbidden if the current user has insufficient permissions + */ + public function getChild($name) { + // users can only query their own memberships + // but ownCloud admin can query membership of any user + if ($name === $this->helper->getUserId() || $this->helper->isUserSuperAdmin()) { + return new UserMembershipCollection( + $name, + $this->groupsHandler, + $this->helper + ); + } + + // regular user can only query for self + throw new Forbidden('Insufficient permissions'); + } + + /** + * Not supported + * + * @throws MethodNotAllowed not supported + */ + public function getChildren() { + throw new MethodNotAllowed('Not supported'); + } + + /** + * Returns whether a custom group exists. + * + * @param string $name user id + * @return boolean true if the group exists, false otherwise + */ + public function childExists($name) { + try { + $this->getChild($name); + } catch (Forbidden $e) { + return false; + } catch (NotFound $e) { + return false; + } + return true; + } + + /** + * Not supported + * + * @throws MethodNotAllowed not supported + */ + public function delete() { + throw new MethodNotAllowed('Cannot delete this collection'); + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string node name + */ + public function getName() { + return 'users'; + } + + /** + * Not supported + * + * @param string $name name + * @throws MethodNotAllowed not supported + */ + public function setName($name) { + throw new MethodNotAllowed('Cannot rename this collection'); + } + + /** + * Returns null + * + * @return int null + */ + public function getLastModified() { + return null; + } +} diff --git a/tests/unit/CustomGroupsBackendTest.php b/tests/unit/CustomGroupsBackendTest.php index 5fcd269d..fe60acb3 100644 --- a/tests/unit/CustomGroupsBackendTest.php +++ b/tests/unit/CustomGroupsBackendTest.php @@ -2,7 +2,7 @@ /** * @author Vincent Petry * - * @copyright Copyright (c) 2016, ownCloud GmbH. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -68,10 +68,10 @@ public function testInGroup() { public function testGetUserGroups() { $this->handler->expects($this->any()) - ->method('getUserGroups') + ->method('getUserMemberships') ->will($this->returnValueMap([ - ['user1', [1, 2]], - ['user2', [1, 3]], + ['user1', null, [['group_id' => 1], ['group_id' => 2]]], + ['user2', null, [['group_id' => 1], ['group_id' => 3]]], ])); $this->assertEquals( diff --git a/tests/unit/CustomGroupsDatabaseHandlerTest.php b/tests/unit/CustomGroupsDatabaseHandlerTest.php index d78e2a2a..c6375fb3 100644 --- a/tests/unit/CustomGroupsDatabaseHandlerTest.php +++ b/tests/unit/CustomGroupsDatabaseHandlerTest.php @@ -2,7 +2,7 @@ /** * @author Vincent Petry * - * @copyright Copyright (c) 2016, ownCloud GmbH. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -82,6 +82,16 @@ public function testDeleteGroup() { $this->assertFalse($this->handler->deleteGroup($groupId)); } + public function testUpdateGroup() { + $groupId = $this->handler->createGroup('my_group', 'My Group'); + $this->assertTrue($this->handler->updateGroup($groupId, 'meine_gruppe', 'Meine Gruppe')); + + $groupInfo = $this->handler->getGroup($groupId); + + $this->assertEquals('meine_gruppe', $groupInfo['uri']); + $this->assertEquals('Meine Gruppe', $groupInfo['display_name']); + } + public function testSearchGroups() { $group1Id = $this->handler->createGroup('my_group_1', 'My One Group'); $group2Id = $this->handler->createGroup('my_group_2', 'My Group Two'); @@ -174,8 +184,8 @@ public function testGetGroups() { public function testAddToGroup() { $groupId = $this->handler->createGroup('my_group', 'My Group'); - $this->assertTrue($this->handler->addToGroup('user2', $groupId, false)); - $this->assertTrue($this->handler->addToGroup('user1', $groupId, true)); + $this->assertTrue($this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER)); + $this->assertTrue($this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN)); $members = $this->handler->getGroupMembers($groupId); @@ -183,23 +193,23 @@ public function testAddToGroup() { $this->assertEquals('user1', $members[0]['user_id']); $this->assertEquals($groupId, $members[0]['group_id']); - $this->assertTrue($members[0]['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_ADMIN, $members[0]['role']); $this->assertEquals('user2', $members[1]['user_id']); $this->assertEquals($groupId, $members[1]['group_id']); - $this->assertFalse($members[1]['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $members[1]['role']); // add again returns false - $this->assertFalse($this->handler->addToGroup('user1', $groupId, true)); + $this->assertFalse($this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN)); } public function testRemoveFromGroup() { $groupId = $this->handler->createGroup('my_group', 'My Group'); $groupId2 = $this->handler->createGroup('my_group2', 'My Group Two'); - $this->handler->addToGroup('user2', $groupId, false); - $this->handler->addToGroup('user1', $groupId, true); - $this->handler->addToGroup('user2', $groupId2, false); + $this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); + $this->handler->addToGroup('user2', $groupId2, CustomGroupsDatabaseHandler::ROLE_MEMBER); $this->assertTrue($this->handler->removeFromGroup('user2', $groupId)); @@ -208,7 +218,7 @@ public function testRemoveFromGroup() { $this->assertEquals('user1', $members[0]['user_id']); $this->assertEquals($groupId, $members[0]['group_id']); - $this->assertTrue($members[0]['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_ADMIN, $members[0]['role']); // member still exists in the other group $members2 = $this->handler->getGroupMembers($groupId2); @@ -216,17 +226,38 @@ public function testRemoveFromGroup() { $this->assertCount(1, $members2); $this->assertEquals('user2', $members2[0]['user_id']); $this->assertEquals($groupId2, $members2[0]['group_id']); - $this->assertFalse($members2[0]['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $members2[0]['role']); // remove again returns false $this->assertFalse($this->handler->removeFromGroup('user2', $groupId)); } + public function testGetGroupMembersFilter() { + $groupId = $this->handler->createGroup('my_group', 'My Group'); + + $this->assertTrue($this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER)); + $this->assertTrue($this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN)); + + $adminMembers = $this->handler->getGroupMembers($groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); + $nonAdminMembers = $this->handler->getGroupMembers($groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + + $this->assertCount(1, $adminMembers); + $this->assertCount(1, $nonAdminMembers); + + $this->assertEquals('user1', $adminMembers[0]['user_id']); + $this->assertEquals($groupId, $adminMembers[0]['group_id']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_ADMIN, $adminMembers[0]['role']); + + $this->assertEquals('user2', $nonAdminMembers[0]['user_id']); + $this->assertEquals($groupId, $nonAdminMembers[0]['group_id']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $nonAdminMembers[0]['role']); + } + public function testDeleteRemovesMembers() { $groupId = $this->handler->createGroup('my_group', 'My Group'); - $this->handler->addToGroup('user2', $groupId, false); - $this->handler->addToGroup('user1', $groupId, true); + $this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); $this->assertTrue($this->handler->deleteGroup($groupId)); @@ -237,54 +268,77 @@ public function testDeleteRemovesMembers() { public function testGetGroupMemberInfo() { $groupId = $this->handler->createGroup('my_group', 'My Group'); - $this->handler->addToGroup('user2', $groupId, false); - $this->handler->addToGroup('user1', $groupId, true); + $this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); $member = $this->handler->getGroupMemberInfo($groupId, 'user1'); $this->assertEquals('user1', $member['user_id']); $this->assertEquals($groupId, $member['group_id']); - $this->assertTrue($member['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_ADMIN, $member['role']); } public function testSetGroupMemberInfo() { $groupId = $this->handler->createGroup('my_group', 'My Group'); - $this->handler->addToGroup('user1', $groupId, true); + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); - $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', false)); + $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', CustomGroupsDatabaseHandler::ROLE_MEMBER)); $member = $this->handler->getGroupMemberInfo($groupId, 'user1'); - $this->assertFalse($member['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $member['role']); - $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', true)); + $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', CustomGroupsDatabaseHandler::ROLE_ADMIN)); $member = $this->handler->getGroupMemberInfo($groupId, 'user1'); - $this->assertTrue($member['is_admin']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_ADMIN, $member['role']); // setting to same value also returns true - $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', true)); + $this->assertTrue($this->handler->setGroupMemberInfo($groupId, 'user1', CustomGroupsDatabaseHandler::ROLE_ADMIN)); } public function testInGroup() { $groupId = $this->handler->createGroup('my_group', 'My Group'); - $this->handler->addToGroup('user2', $groupId, false); + $this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); $this->assertTrue($this->handler->inGroup('user2', $groupId)); $this->assertFalse($this->handler->inGroup('user3', $groupId)); } - public function testGetUserGroups() { + public function testGetUserMemberships() { $groupId = $this->handler->createGroup('my_group', 'My Group'); $groupId2 = $this->handler->createGroup('my_group2', 'My Group Two'); - $this->handler->addToGroup('user2', $groupId, false); - $this->handler->addToGroup('user1', $groupId, true); - $this->handler->addToGroup('user2', $groupId2, false); + $this->handler->addToGroup('user2', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_ADMIN); + $this->handler->addToGroup('user2', $groupId2, CustomGroupsDatabaseHandler::ROLE_MEMBER); - $groups = $this->handler->getUserGroups('user2'); + $groups = $this->handler->getUserMemberships('user2'); $this->assertCount(2, $groups); - $this->assertEquals($groupId, $groups[0]); - $this->assertEquals($groupId2, $groups[1]); + $this->assertEquals($groupId, $groups[0]['group_id']); + $this->assertEquals('user2', $groups[0]['user_id']); + $this->assertEquals('my_group', $groups[0]['uri']); + $this->assertEquals('My Group', $groups[0]['display_name']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $groups[0]['role']); + $this->assertEquals($groupId2, $groups[1]['group_id']); + $this->assertEquals('user2', $groups[1]['user_id']); + $this->assertEquals('my_group2', $groups[1]['uri']); + $this->assertEquals('My Group Two', $groups[1]['display_name']); + $this->assertEquals(CustomGroupsDatabaseHandler::ROLE_MEMBER, $groups[1]['role']); } + public function testGetUserMembershipsFiltered() { + $groupId = $this->handler->createGroup('my_group', 'My Group'); + $groupId2 = $this->handler->createGroup('my_group2', 'My Group Two'); + + $this->handler->addToGroup('user1', $groupId, CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->handler->addToGroup('user1', $groupId2, CustomGroupsDatabaseHandler::ROLE_ADMIN); + + $adminGroups = $this->handler->getUserMemberships('user1', CustomGroupsDatabaseHandler::ROLE_ADMIN); + $this->assertCount(1, $adminGroups); + $nonAdminGroups = $this->handler->getUserMemberships('user1', CustomGroupsDatabaseHandler::ROLE_MEMBER); + $this->assertCount(1, $nonAdminGroups); + + $this->assertEquals($groupId, $nonAdminGroups[0]['group_id']); + $this->assertEquals($groupId2, $adminGroups[0]['group_id']); + } } diff --git a/tests/unit/Dav/GroupMembershipCollectionTest.php b/tests/unit/Dav/GroupMembershipCollectionTest.php new file mode 100644 index 00000000..ad233ef3 --- /dev/null +++ b/tests/unit/Dav/GroupMembershipCollectionTest.php @@ -0,0 +1,413 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\GroupMembershipCollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\IUser; +use Sabre\DAV\PropPatch; +use OCA\CustomGroups\Dav\MembershipNode; +use OCA\CustomGroups\Dav\MembershipHelper; +use OCP\IGroupManager; + +/** + * Class GroupMembershipCollectionTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class GroupMembershipCollectionTest extends \Test\TestCase { + const CURRENT_USER = 'currentuser'; + const NODE_USER = 'nodeuser'; + + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var GroupMembershipCollection + */ + private $node; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->handler->expects($this->never())->method('getGroup'); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + + // currently logged in user + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::CURRENT_USER); + $this->userSession->method('getUser')->willReturn($user); + + $nodeUser = $this->createMock(IUser::class); + $nodeUser->method('getUID')->willReturn(self::NODE_USER); + $this->userManager->method('get')->will( + $this->returnValueMap([ + [self::NODE_USER, $nodeUser], + [strtoupper(self::NODE_USER), $nodeUser], + ])); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + $this->node = new GroupMembershipCollection( + ['group_id' => 1, 'uri' => 'group1', 'display_name' => 'Group One'], + $this->handler, + $this->helper + ); + } + + /** + * Sets a user's member info, for testing + * + * @param array $memberInfo user member info + */ + private function setCurrentUserMemberInfo($memberInfo) { + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->with(1, self::CURRENT_USER) + ->willReturn($memberInfo); + } + + private function setCurrentUserSuperAdmin($isSuperAdmin) { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->with(self::CURRENT_USER) + ->willReturn($isSuperAdmin); + } + + public function testBase() { + $this->assertEquals('group1', $this->node->getName()); + $this->assertNull($this->node->getLastModified()); + } + + public function testDeleteAsAdmin() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN]); + + $this->handler->expects($this->at(1)) + ->method('deleteGroup') + ->with(1); + + $this->node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteAsNonAdmin() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]); + + $this->handler->expects($this->never()) + ->method('deleteGroup'); + + $this->node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteAsNonMember() { + $this->setCurrentUserMemberInfo(null); + + $this->handler->expects($this->never()) + ->method('deleteGroup'); + + $this->node->delete(); + } + + public function testGetProperties() { + $props = $this->node->getProperties(null); + $this->assertEquals('Group One', $props[GroupMembershipCollection::PROPERTY_DISPLAY_NAME]); + $props = $this->node->getProperties([GroupMembershipCollection::PROPERTY_DISPLAY_NAME]); + $this->assertEquals('Group One', $props[GroupMembershipCollection::PROPERTY_DISPLAY_NAME]); + } + + public function adminSetFlagProvider() { + return [ + // admin can change display name + [false, true, 200, true], + // non-admin cannot change anything + [false, false, 403, false], + // non-member cannot change anything + [false, null, 403, false], + // super-admin non-member can change anything + [false, true, 200, true], + ]; + } + + /** + * @dataProvider adminSetFlagProvider + */ + public function testSetProperties($isSuperAdmin, $currentUserRole, $statusCode, $called) { + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + if ($currentUserRole !== null) { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => $currentUserRole]); + } else { + $this->setCurrentUserMemberInfo(null); + } + + if ($called) { + $this->handler->expects($this->at(1)) + ->method('updateGroup') + ->with(1, 'group1', 'Group Renamed') + ->willReturn(true); + } else { + $this->handler->expects($this->never()) + ->method('updateGroup'); + } + + $propPatch = new PropPatch([GroupMembershipCollection::PROPERTY_DISPLAY_NAME => 'Group Renamed']); + $this->node->propPatch($propPatch); + + $propPatch->commit(); + $this->assertEmpty($propPatch->getRemainingMutations()); + $result = $propPatch->getResult(); + $this->assertEquals($statusCode, $result[GroupMembershipCollection::PROPERTY_DISPLAY_NAME]); + } + + public function rolesProvider() { + return [ + [false, ['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN]], + [false, ['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]], + [true, null], + ]; + } + + public function adminProvider() { + return [ + [false, ['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN]], + [true, null], + ]; + } + + /** + * @dataProvider adminProvider + */ + public function testAddMemberAsAdmin($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserMemberInfo($currentMemberInfo); + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $this->handler->expects($this->once()) + ->method('addToGroup') + ->with(self::NODE_USER, 1, false) + ->willReturn(true); + + $this->node->createFile(self::NODE_USER); + } + + /** + * @expectedException \Sabre\DAV\Exception\PreconditionFailed + * @dataProvider adminProvider + */ + public function testAddMemberAsAdminFails($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserMemberInfo($currentMemberInfo); + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $this->handler->expects($this->once()) + ->method('addToGroup') + ->with(self::NODE_USER, 1, false) + ->willReturn(false); + + $this->node->createFile(self::NODE_USER); + } + + /** + * @expectedException \Sabre\DAV\Exception\PreconditionFailed + * @dataProvider adminProvider + */ + public function testAddNonExistingMemberAsAdmin($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserMemberInfo($currentMemberInfo); + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $this->handler->expects($this->never()) + ->method('addToGroup'); + + $this->node->createFile('userunexist'); + } + + /** + * @expectedException \Sabre\DAV\Exception\PreconditionFailed + * @dataProvider adminProvider + */ + public function testAddNonExistingMemberMismatchCaseAsAdmin($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserMemberInfo($currentMemberInfo); + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $this->handler->expects($this->never()) + ->method('addToGroup'); + + $this->node->createFile('USER2'); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testAddMemberAsNonAdmin() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]); + + $this->handler->expects($this->never()) + ->method('addToGroup'); + + $this->node->createFile(self::NODE_USER); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testAddMemberAsNonMember() { + $this->setCurrentUserMemberInfo(null); + + $this->handler->expects($this->never()) + ->method('addToGroup'); + + $this->node->createFile(self::NODE_USER); + } + + public function testIsMember() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]); + $this->handler->expects($this->any()) + ->method('inGroup') + ->will($this->returnValueMap([ + [self::NODE_USER, 1, true], + ['user3', 1, false], + ])); + + $this->assertTrue($this->node->childExists(self::NODE_USER)); + $this->assertFalse($this->node->childExists('user3')); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testIsMemberAsNonMember() { + $this->setCurrentUserMemberInfo(null); + + $this->node->childExists(self::NODE_USER); + } + + /** + * @dataProvider rolesProvider + */ + public function testGetMember($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $membershipsMap = [ + [1, self::NODE_USER, ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]], + ]; + if (!is_null($currentMemberInfo)) { + $membershipsMap[] = [1, self::CURRENT_USER, $currentMemberInfo]; + } + + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->will($this->returnValueMap($membershipsMap)); + + $memberInfo = $this->node->getChild(self::NODE_USER); + + $this->assertInstanceOf(MembershipNode::class, $memberInfo); + $this->assertEquals(self::NODE_USER, $memberInfo->getName()); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testGetMemberAsNonMember() { + $this->setCurrentUserMemberInfo(null); + + $this->node->getChild(self::NODE_USER); + } + + /** + * @dataProvider rolesProvider + */ + public function testGetMembers($isSuperAdmin, $currentMemberInfo) { + $this->setCurrentUserMemberInfo($currentMemberInfo); + $this->setCurrentUserSuperAdmin($isSuperAdmin); + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1) + ->willReturn([ + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + ['group_id' => 1, 'user_id' => 'user3', 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER], + ]); + + $memberInfos = $this->node->getChildren(); + + $this->assertCount(2, $memberInfos); + $this->assertInstanceOf(MembershipNode::class, $memberInfos[0]); + $this->assertEquals(self::NODE_USER, $memberInfos[0]->getName()); + $this->assertInstanceOf(MembershipNode::class, $memberInfos[1]); + $this->assertEquals('user3', $memberInfos[1]->getName()); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testGetMembersAsNonMember() { + $this->setCurrentUserMemberInfo(null); + + $this->node->getChildren(); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testSetName() { + $this->node->setName('x'); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateDirectory() { + $this->node->createDirectory('somedir'); + } +} diff --git a/tests/unit/Dav/GroupsCollectionTest.php b/tests/unit/Dav/GroupsCollectionTest.php new file mode 100644 index 00000000..47203485 --- /dev/null +++ b/tests/unit/Dav/GroupsCollectionTest.php @@ -0,0 +1,193 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\GroupsCollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\IUser; +use OCA\CustomGroups\Dav\GroupMembershipCollection; +use OCA\CustomGroups\Dav\MembershipHelper; +use OCP\IGroupManager; + +/** + * Class GroupsCollectionTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class GroupsCollectionTest extends \Test\TestCase { + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var GroupsCollection + */ + private $collection; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->handler->expects($this->never())->method('getGroup'); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + $this->collection = new GroupsCollection($this->handler, $this->helper); + } + + public function testBase() { + $this->assertEquals('groups', $this->collection->getName()); + $this->assertNull($this->collection->getLastModified()); + } + + public function testListGroups() { + $this->handler->expects($this->at(0)) + ->method('getGroups') + ->will($this->returnValue([ + ['group_id' => 1, 'uri' => 'group1', 'display_name' => 'Group One'], + ['group_id' => 2, 'uri' => 'group2', 'display_name' => 'Group Two'], + ])); + + $nodes = $this->collection->getChildren(); + $this->assertCount(2, $nodes); + + $this->assertInstanceOf(GroupMembershipCollection::class, $nodes[0]); + $this->assertEquals('group1', $nodes[0]->getName()); + $this->assertInstanceOf(GroupMembershipCollection::class, $nodes[1]); + $this->assertEquals('group2', $nodes[1]->getName()); + } + + public function testCreateGroup() { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user1'); + $this->userSession->method('getUser')->willReturn($user); + + $this->handler->expects($this->at(0)) + ->method('createGroup') + ->with('group1', 'group1') + ->will($this->returnValue(1)); + $this->handler->expects($this->at(1)) + ->method('addToGroup') + ->with('user1', 1, true); + + $this->collection->createDirectory('group1'); + } + + /** + * @expectedException \Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateGroupAlreadyExists() { + $this->handler->expects($this->once()) + ->method('createGroup') + ->with('group1', 'group1') + ->will($this->returnValue(null)); + $this->handler->expects($this->never()) + ->method('addToGroup') + ->with('user1', 1, true); + + $this->collection->createDirectory('group1'); + } + + public function testGetGroup() { + $this->handler->expects($this->any()) + ->method('getGroupByUri') + ->with('group1') + ->will($this->returnValue(['group_id' => 1, 'uri' => 'group1', 'display_name' => 'Group One'])); + + $groupNode = $this->collection->getChild('group1'); + $this->assertInstanceOf(GroupMembershipCollection::class, $groupNode); + $this->assertEquals('group1', $groupNode->getName()); + } + + /** + * @expectedException \Sabre\DAV\Exception\NotFound + */ + public function testGetGroupNonExisting() { + $this->handler->expects($this->any()) + ->method('getGroupByUri') + ->with('groupx') + ->will($this->returnValue(null)); + + $this->collection->getChild('groupx'); + } + + public function testGroupExists() { + $this->handler->expects($this->any()) + ->method('getGroupByUri') + ->will($this->returnValueMap([ + ['group1', ['group_id' => 1, 'uri' => 'group1', 'display_name' => 'Group One']], + ['group2', null], + ])); + + $this->assertTrue($this->collection->childExists('group1')); + $this->assertFalse($this->collection->childExists('group2')); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testSetName() { + $this->collection->setName('x'); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testDelete() { + $this->collection->delete(); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateFile() { + $this->collection->createFile('somefile.txt'); + } +} diff --git a/tests/unit/Dav/MembershipHelperTest.php b/tests/unit/Dav/MembershipHelperTest.php new file mode 100644 index 00000000..e4cd29bd --- /dev/null +++ b/tests/unit/Dav/MembershipHelperTest.php @@ -0,0 +1,224 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCP\IUserSession; +use OCP\IUserManager; +use OCP\IGroupManager; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUser; +use OCA\CustomGroups\Dav\MembershipHelper; + +/** + * Class MembershipHelperTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class MembershipHelperTest extends \Test\TestCase { + + const CURRENT_USER = 'currentuser'; + + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + // currently logged in user + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::CURRENT_USER); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + } + + public function testGetUserId() { + $this->assertEquals(self::CURRENT_USER, $this->helper->getUserId()); + } + + public function testGetUser() { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('anotheruser'); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('anotheruser') + ->willReturn($user); + + $this->assertEquals($user, $this->helper->getUser('anotheruser')); + } + + public function isUserAdminDataProvider() { + return [ + // regular member + [ + false, + ['role' => CustomGroupsDatabaseHandler::ROLE_MEMBER], + false, + ], + // admin member + [ + false, + ['role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + true, + ], + // super-admin but non-admin member + [ + true, + ['role' => CustomGroupsDatabaseHandler::ROLE_MEMBER], + true, + ], + // non-member + [ + false, + null, + false, + ], + // super-admin non-member + [ + true, + null, + true, + ], + ]; + } + + /** + * @dataProvider isUserAdminDataProvider + */ + public function testIsUserAdmin($isSuperAdmin, $memberInfo, $expectedResult) { + $this->groupManager->expects($this->once()) + ->method('isAdmin') + ->with(self::CURRENT_USER) + ->willReturn($isSuperAdmin); + + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->with('group1', self::CURRENT_USER) + ->willReturn($memberInfo); + + $this->assertEquals($expectedResult, $this->helper->isUserAdmin('group1')); + } + + public function isUserMemberDataProvider() { + return [ + // regular member + [ + ['role' => CustomGroupsDatabaseHandler::ROLE_MEMBER], + true, + ], + // admin member + [ + ['role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + true, + ], + // non-member + [ + null, + false, + ], + ]; + } + + /** + * @dataProvider isUserMemberDataProvider + */ + public function testIsUserMember($memberInfo, $expectedResult) { + $this->handler->expects($this->once()) + ->method('getGroupMemberInfo') + ->with('group1', self::CURRENT_USER) + ->willReturn($memberInfo); + + $this->assertEquals($expectedResult, $this->helper->isUserMember('group1')); + } + + public function isTheOnlyAdminDataProvider() { + return [ + // user is not the last admin + [ + [ + ['user_id' => 'admin1'], + ['user_id' => 'admin2'], + ], + false, + ], + // user is the last admin + [ + [ + ['user_id' => 'admin1'], + ], + true, + ], + // someone else is the last admin + [ + [ + ['user_id' => 'admin2'], + ], + false, + ], + ]; + } + + /** + * @dataProvider isTheOnlyAdminDataProvider + */ + public function testIsTheOnlyAdmin($memberInfo, $expectedResult) { + $this->handler->expects($this->once()) + ->method('getGroupMembers') + ->with('group1', CustomGroupsDatabaseHandler::ROLE_ADMIN) + ->willReturn($memberInfo); + + $this->assertEquals($expectedResult, $this->helper->isTheOnlyAdmin('group1', 'admin1')); + } +} diff --git a/tests/unit/Dav/MembershipNodeTest.php b/tests/unit/Dav/MembershipNodeTest.php new file mode 100644 index 00000000..fe1b93b4 --- /dev/null +++ b/tests/unit/Dav/MembershipNodeTest.php @@ -0,0 +1,432 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\MembershipNode; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\IUser; +use Sabre\DAV\PropPatch; +use OCA\CustomGroups\Dav\MembershipHelper; +use OCP\IGroupManager; + +/** + * Class MembershipNodeTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class MembershipNodeTest extends \Test\TestCase { + + const CURRENT_USER = 'currentuser'; + const NODE_USER = 'nodeuser'; + + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var MembershipNode + */ + private $node; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->handler->expects($this->never())->method('getGroup'); + $this->userSession = $this->createMock(IUserSession::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + // currently logged in user + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::CURRENT_USER); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + $this->node = new MembershipNode( + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + self::NODE_USER, + $this->handler, + $this->helper + ); + } + + /** + * Sets a user's member info, for testing + * + * @param array $memberInfo user member info + */ + private function setCurrentUserMemberInfo($memberInfo) { + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->with(1, self::CURRENT_USER) + ->willReturn($memberInfo); + } + + public function testBase() { + $this->assertEquals(self::NODE_USER, $this->node->getName()); + $this->assertNull($this->node->getLastModified()); + } + + public function testNodeName() { + $node = new MembershipNode( + ['group_id' => 1, 'uri' => 'group1', 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + 'group1', + $this->handler, + $this->helper + ); + $this->assertEquals('group1', $node->getName()); + } + + public function testDeleteAsAdmin() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN]); + $this->handler->expects($this->once()) + ->method('removeFromGroup') + ->with(self::NODE_USER, 1) + ->willReturn(true); + + $this->node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\PreconditionFailed + */ + public function testDeleteAsAdminFailed() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN]); + $this->handler->expects($this->once()) + ->method('removeFromGroup') + ->with(self::NODE_USER, 1) + ->willReturn(false); + + $this->node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteAsNonAdmin() { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER]); + $this->handler->expects($this->never()) + ->method('removeFromGroup'); + + $this->node->delete(); + } + + /** + * Creates a node for the NODE_USER user and give that + * user permissions if needed + * + * @param int $role admin perms for the NODE_USER + * @return MembershipNode new node + */ + private function makeSelfNode($role) { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::NODE_USER); + $userSession = $this->createMock(IUserSession::class); + $userSession->method('getUser')->willReturn($user); + + $helper = new MembershipHelper( + $this->handler, + $userSession, + $this->userManager, + $this->groupManager + ); + + $memberInfo = ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => $role]; + $node = new MembershipNode( + $memberInfo, + self::NODE_USER, + $this->handler, + $helper + ); + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->with(1, self::NODE_USER) + ->willReturn($memberInfo); + return $node; + } + + public function testDeleteSelfAsNonAdmin() { + $node = $this->makeSelfNode(CustomGroupsDatabaseHandler::ROLE_MEMBER); + + $this->handler->expects($this->once()) + ->method('removeFromGroup') + ->with(self::NODE_USER, 1) + ->willReturn(true); + + $node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteAsNonMember() { + $this->setCurrentUserMemberInfo(null); + $this->handler->expects($this->never()) + ->method('removeFromGroup'); + + $this->node->delete(); + } + + /** + * Super admin can delete any member + */ + public function testDeleteAsSuperAdmin() { + $this->setCurrentUserMemberInfo(null); + $this->groupManager->method('isAdmin') + ->with(self::CURRENT_USER) + ->willReturn(true); + + $this->handler->expects($this->once()) + ->method('removeFromGroup') + ->with(self::NODE_USER, 1) + ->willReturn(true); + + $this->node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteSelfAsLastAdmin() { + $node = $this->makeSelfNode(CustomGroupsDatabaseHandler::ROLE_ADMIN); + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1, CustomGroupsDatabaseHandler::ROLE_ADMIN) + ->willReturn([ + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN] + ]); + + $this->handler->expects($this->never()) + ->method('removeFromGroup'); + + $node->delete(); + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + public function testDeleteLastAdminAsSuperAdmin() { + $node = $this->makeSelfNode(CustomGroupsDatabaseHandler::ROLE_MEMBER); + + $this->groupManager->method('isAdmin') + ->with(self::NODE_USER) + ->willReturn(true); + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1, CustomGroupsDatabaseHandler::ROLE_ADMIN) + ->willReturn([ + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN] + ]); + + $this->handler->expects($this->never()) + ->method('removeFromGroup'); + + $node->delete(); + } + + public function propsProvider() { + return [ + [ + MembershipNode::PROPERTY_ROLE, + CustomGroupsDatabaseHandler::ROLE_ADMIN, + CustomGroupsDatabaseHandler::ROLE_ADMIN, + ], + [ + MembershipNode::PROPERTY_ROLE, + CustomGroupsDatabaseHandler::ROLE_MEMBER, + CustomGroupsDatabaseHandler::ROLE_MEMBER, + ], + [ + MembershipNode::PROPERTY_USER_ID, + self::NODE_USER, + ], + [ + MembershipNode::PROPERTY_GROUP_URI, + 'group1', + ], + ]; + } + + /** + * @dataProvider propsProvider + */ + public function testGetProperties($propName, $propValue, $roleValue = 0) { + $node = new MembershipNode( + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => $roleValue, 'uri' => 'group1'], + self::NODE_USER, + $this->handler, + $this->helper + ); + + $props = $node->getProperties(null); + $this->assertSame($propValue, $props[$propName]); + $props = $node->getProperties([$propName]); + $this->assertSame($propValue, $props[$propName]); + } + + public function adminSetFlagProvider() { + return [ + // admin can change flag for others + [false, CustomGroupsDatabaseHandler::ROLE_ADMIN, CustomGroupsDatabaseHandler::ROLE_ADMIN, 200, true], + [false, CustomGroupsDatabaseHandler::ROLE_ADMIN, CustomGroupsDatabaseHandler::ROLE_MEMBER, 200, true], + // non-admin cannot change anything + [false, CustomGroupsDatabaseHandler::ROLE_MEMBER, CustomGroupsDatabaseHandler::ROLE_ADMIN, 403, false], + [false, CustomGroupsDatabaseHandler::ROLE_MEMBER, CustomGroupsDatabaseHandler::ROLE_MEMBER, 403, false], + // non-member cannot change anything + [false, null, CustomGroupsDatabaseHandler::ROLE_ADMIN, 403, false], + [false, null, CustomGroupsDatabaseHandler::ROLE_MEMBER, 403, false], + // super-admin can change even as non-member + [true, null, CustomGroupsDatabaseHandler::ROLE_ADMIN, 200, true], + [true, null, CustomGroupsDatabaseHandler::ROLE_MEMBER, 200, true], + ]; + } + + /** + * @dataProvider adminSetFlagProvider + */ + public function testSetProperties($isSuperAdmin, $currentUserRole, $roleToSet, $statusCode, $called) { + if ($currentUserRole !== null) { + $this->setCurrentUserMemberInfo(['group_id' => 1, 'user_id' => self::CURRENT_USER, 'role' => $currentUserRole]); + } else { + $this->setCurrentUserMemberInfo(null); + } + + $this->groupManager->method('isAdmin') + ->with(self::CURRENT_USER) + ->willReturn($isSuperAdmin); + + if ($called) { + $this->handler->expects($this->once()) + ->method('setGroupMemberInfo') + ->with(1, self::NODE_USER, $roleToSet) + ->willReturn(true); + } else { + $this->handler->expects($this->never()) + ->method('setGroupMemberInfo'); + } + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1, true) + ->willReturn([ + ['group_id' => 1, 'user_id' => 'someotheradmin', 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN], + ]); + + $propPatch = new PropPatch([MembershipNode::PROPERTY_ROLE => $roleToSet]); + $this->node->propPatch($propPatch); + + $propPatch->commit(); + $this->assertEmpty($propPatch->getRemainingMutations()); + $result = $propPatch->getResult(); + $this->assertEquals($statusCode, $result[MembershipNode::PROPERTY_ROLE]); + } + + /** + * Cannot remove admin perms from last admin + */ + public function testUnsetSelfAdminWhenLastAdmin() { + $this->groupManager->method('isAdmin') + ->with(self::CURRENT_USER) + ->willReturn(true); + + $this->handler->expects($this->never()) + ->method('setGroupMemberInfo'); + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1, true) + ->willReturn([ + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN] + ]); + + $propPatch = new PropPatch([MembershipNode::PROPERTY_ROLE => 0]); + $this->node->propPatch($propPatch); + + $propPatch->commit(); + $this->assertEmpty($propPatch->getRemainingMutations()); + $result = $propPatch->getResult(); + $this->assertEquals(403, $result[MembershipNode::PROPERTY_ROLE]); + } + + /** + * Cannot remove admin perms from last admin + */ + public function testUnsetdminWhenLastAdminAsSuperAdmin() { + $node = $this->makeSelfNode(CustomGroupsDatabaseHandler::ROLE_ADMIN); + + $this->handler->expects($this->never()) + ->method('setGroupMemberInfo'); + + $this->handler->expects($this->any()) + ->method('getGroupMembers') + ->with(1, true) + ->willReturn([ + ['group_id' => 1, 'user_id' => self::NODE_USER, 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN] + ]); + + $propPatch = new PropPatch([MembershipNode::PROPERTY_ROLE => 0]); + $node->propPatch($propPatch); + + $propPatch->commit(); + $this->assertEmpty($propPatch->getRemainingMutations()); + $result = $propPatch->getResult(); + $this->assertEquals(403, $result[MembershipNode::PROPERTY_ROLE]); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testSetName() { + $this->node->setName('x'); + } +} diff --git a/tests/unit/Dav/RootCollectionTest.php b/tests/unit/Dav/RootCollectionTest.php new file mode 100644 index 00000000..5fa58917 --- /dev/null +++ b/tests/unit/Dav/RootCollectionTest.php @@ -0,0 +1,63 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\RootCollection; +use OCA\CustomGroups\Dav\UsersCollection; +use OCA\CustomGroups\Dav\GroupsCollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCA\CustomGroups\Dav\MembershipHelper; + +/** + * Class RootCollectionTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class RootCollectionTest extends \Test\TestCase { + + public function setUp() { + parent::setUp(); + $handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $helper = $this->createMock(MembershipHelper::class); + + $this->collection = new RootCollection( + $handler, + $helper + ); + } + + public function testGetGroups() { + $groups = $this->collection->getChild('groups'); + $this->assertInstanceOf(GroupsCollection::class, $groups); + } + + public function testGetUsers() { + $users = $this->collection->getChild('users'); + $this->assertInstanceOf(UsersCollection::class, $users); + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + public function testGetNonExisting() { + $this->collection->getChild('somethingelse'); + } +} diff --git a/tests/unit/Dav/UserMembershipCollectionTest.php b/tests/unit/Dav/UserMembershipCollectionTest.php new file mode 100644 index 00000000..9fc24135 --- /dev/null +++ b/tests/unit/Dav/UserMembershipCollectionTest.php @@ -0,0 +1,213 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\UserMembershipCollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\IUser; +use Sabre\DAV\PropPatch; +use OCA\CustomGroups\Dav\MembershipNode; +use OCA\CustomGroups\Dav\MembershipHelper; +use OCP\IGroupManager; + +/** + * Class UserMembershipCollectionTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class UserMembershipCollectionTest extends \Test\TestCase { + const CURRENT_USER = 'currentuser'; + + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var UserMembershipCollection + */ + private $node; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->handler->expects($this->never())->method('getGroup'); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + + // currently logged in user + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::CURRENT_USER); + $this->userSession->method('getUser')->willReturn($user); + + $this->handler->expects($this->any()) + ->method('getGroupByUri') + ->will($this->returnValueMap([ + ['group1', ['group_id' => 1]], + ['group2', ['group_id' => 2]], + ['group3', null], + ])); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + $this->node = new UserMembershipCollection( + self::CURRENT_USER, + $this->handler, + $this->helper + ); + } + + public function testBase() { + $this->assertEquals(self::CURRENT_USER, $this->node->getName()); + $this->assertNull($this->node->getLastModified()); + } + + /** + * @expectedException \Sabre\DAV\Exception\MethodNotAllowed + */ + public function testDelete() { + $this->node->delete(); + } + + public function testIsMember() { + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->will($this->returnValueMap([ + [1, self::CURRENT_USER, [ + 'group_id' => 1, + 'uri' => 'group1', + 'user_id' => self::CURRENT_USER, + 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER + ]], + ])); + + $this->assertTrue($this->node->childExists('group1')); + $this->assertFalse($this->node->childExists('group2')); + $this->assertFalse($this->node->childExists('group3')); + } + + public function testIsMemberAsNonMember() { + $this->assertFalse($this->node->childExists('group1')); + } + + public function testGetMember() { + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->will($this->returnValueMap([ + [1, self::CURRENT_USER, [ + 'group_id' => 1, + 'uri' => 'group1', + 'user_id' => self::CURRENT_USER, + 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER + ]], + ])); + + $memberInfo = $this->node->getChild('group1'); + + $this->assertInstanceOf(MembershipNode::class, $memberInfo); + $this->assertEquals('group1', $memberInfo->getName()); + } + + /** + * @expectedException \Sabre\DAV\Exception\NotFound + */ + public function testGetMemberAsNonMember() { + $this->handler->expects($this->any()) + ->method('getGroupMemberInfo') + ->will($this->returnValue([])); + + $this->node->getChild(self::CURRENT_USER); + } + + public function testGetMembers() { + $this->handler->expects($this->any()) + ->method('getUserMemberships') + ->with(self::CURRENT_USER) + ->willReturn([[ + 'group_id' => 1, + 'uri' => 'group1', + 'user_id' => self::CURRENT_USER, + 'role' => CustomGroupsDatabaseHandler::ROLE_ADMIN + ], [ + 'group_id' => 2, + 'uri' => 'group2', + 'user_id' => self::CURRENT_USER, + 'role' => CustomGroupsDatabaseHandler::ROLE_MEMBER + ], + ]); + + $memberInfos = $this->node->getChildren(); + + $this->assertCount(2, $memberInfos); + $this->assertInstanceOf(MembershipNode::class, $memberInfos[0]); + $this->assertEquals('group1', $memberInfos[0]->getName()); + $this->assertInstanceOf(MembershipNode::class, $memberInfos[1]); + $this->assertEquals('group2', $memberInfos[1]->getName()); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testSetName() { + $this->node->setName('x'); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateFile() { + $this->node->createFile('somedir'); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateDirectory() { + $this->node->createDirectory('somedir'); + } +} diff --git a/tests/unit/Dav/UsersCollectionTest.php b/tests/unit/Dav/UsersCollectionTest.php new file mode 100644 index 00000000..a7370da3 --- /dev/null +++ b/tests/unit/Dav/UsersCollectionTest.php @@ -0,0 +1,160 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\CustomGroups\Tests\unit\Dav; + +use OCA\CustomGroups\Dav\UsersCollection; +use OCA\CustomGroups\CustomGroupsDatabaseHandler; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\IUser; +use OCA\CustomGroups\Dav\UserMembershipCollection; +use OCA\CustomGroups\Dav\MembershipHelper; +use OCP\IGroupManager; + +/** + * Class UsersCollectionTest + * + * @package OCA\CustomGroups\Tests\Unit + */ +class UsersCollectionTest extends \Test\TestCase { + + const USER = 'user1'; + + /** + * @var CustomGroupsDatabaseHandler + */ + private $handler; + + /** + * @var UsersCollection + */ + private $collection; + + /** + * @var MembershipHelper + */ + private $helper; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + public function setUp() { + parent::setUp(); + $this->handler = $this->createMock(CustomGroupsDatabaseHandler::class); + $this->handler->expects($this->never())->method('getGroup'); + $this->userSession = $this->createMock(IUserSession::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn(self::USER); + $this->userSession->method('getUser')->willReturn($user); + + $this->helper = new MembershipHelper( + $this->handler, + $this->userSession, + $this->userManager, + $this->groupManager + ); + + $this->collection = new UsersCollection($this->handler, $this->helper); + } + + public function testBase() { + $this->assertEquals('users', $this->collection->getName()); + $this->assertNull($this->collection->getLastModified()); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testListUsers() { + $this->collection->getChildren(); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateUser() { + $this->collection->createDirectory('user1'); + } + + public function testGetCurrentUser() { + $membershipCollection = $this->collection->getChild(self::USER); + $this->assertInstanceOf(UserMembershipCollection::class, $membershipCollection); + $this->assertEquals(self::USER, $membershipCollection->getName()); + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + public function testGetAnotherUser() { + $this->collection->getChild('another'); + } + + public function testGetAnotherUserAsAdmin() { + $this->groupManager->method('isAdmin')->with(self::USER)->willReturn(true); + $membershipCollection = $this->collection->getChild('another'); + $this->assertInstanceOf(UserMembershipCollection::class, $membershipCollection); + $this->assertEquals('another', $membershipCollection->getName()); + } + + public function testUserExistsCurrent() { + $this->assertTrue($this->collection->childExists(self::USER)); + } + + public function testUserExistsAnother() { + $this->assertFalse($this->collection->childExists('another')); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testSetName() { + $this->collection->setName('x'); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testDelete() { + $this->collection->delete(); + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + public function testCreateFile() { + $this->collection->createFile('somefile.txt'); + } +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 0b1324d6..f452e4e3 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -3,7 +3,7 @@ * @author Joas Schilling * @author Jörn Friedrich Dreyer * - * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify