polls/lib/Model/Acl.php

473 строки
14 KiB
PHP

<?php
/**
* @copyright Copyright (c) 2020 René Gieling <github@dartcafe.de>
*
* @author René Gieling <github@dartcafe.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Polls\Model;
use JsonSerializable;
use OCA\Polls\Exceptions\NotAuthorizedException;
use OCA\Polls\Model\Settings\AppSettings;
use OCA\Polls\Db\Poll;
use OCA\Polls\Db\Share;
use OCA\Polls\Db\OptionMapper;
use OCA\Polls\Db\PollMapper;
use OCA\Polls\Db\VoteMapper;
use OCA\Polls\Db\ShareMapper;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\IGroupManager;
use OCP\AppFramework\Db\DoesNotExistException;
/**
* Class Acl
*
* @package OCA\Polls\Model\Acl
*/
class Acl implements JsonSerializable {
public const PERMISSION_OVERRIDE = 'override_permission';
public const PERMISSION_POLL_VIEW = 'view';
public const PERMISSION_POLL_EDIT = 'edit';
public const PERMISSION_POLL_DELETE = 'delete';
public const PERMISSION_POLL_ARCHIVE = 'archive';
public const PERMISSION_POLL_RESULTS_VIEW = 'seeResults';
public const PERMISSION_POLL_MAILADDRESSES_VIEW = 'seeMailAddresses';
public const PERMISSION_POLL_USERNAMES_VIEW = 'seeUserNames';
public const PERMISSION_POLL_TAKEOVER = 'takeOver';
public const PERMISSION_POLL_SUBSCRIBE = 'subscribe';
public const PERMISSION_POLL_CREATE = 'pollCreate';
public const PERMISSION_POLL_DOWNLOAD = 'pollDownload';
public const PERMISSION_COMMENT_ADD = 'comment';
public const PERMISSION_OPTIONS_ADD = 'add_options';
public const PERMISSION_VOTE_EDIT = 'vote';
public const PERMISSION_PUBLIC_SHARES = 'publicShares';
public const PERMISSION_ALL_ACCESS = 'allAccess';
/** @var IUserManager */
private $userManager;
/** @var IUserSession */
private $userSession;
/** @var AppSettings */
private $appSettings;
/** @var IGroupManager */
private $groupManager;
/** @var OptionMapper */
private $optionMapper;
/** @var PollMapper */
private $pollMapper;
/** @var VoteMapper */
private $voteMapper;
/** @var ShareMapper */
private $shareMapper;
/** @var Poll */
private $poll;
/** @var Share */
private $share;
public function __construct(
IUserManager $userManager,
IUserSession $userSession,
IGroupManager $groupManager,
OptionMapper $optionMapper,
PollMapper $pollMapper,
VoteMapper $voteMapper,
ShareMapper $shareMapper
) {
$this->userManager = $userManager;
$this->userSession = $userSession;
$this->groupManager = $groupManager;
$this->optionMapper = $optionMapper;
$this->pollMapper = $pollMapper;
$this->voteMapper = $voteMapper;
$this->shareMapper = $shareMapper;
$this->poll = new Poll;
$this->share = new Share;
$this->appSettings = new AppSettings;
}
/**
* load share via token and than call setShare
*/
public function setToken(string $token = '', string $permission = self::PERMISSION_POLL_VIEW, ?int $pollIdToValidate = null): Acl {
try {
$this->share = $this->shareMapper->findByToken($token);
if ($pollIdToValidate && $this->share->getPollId() !== $pollIdToValidate) {
throw new NotAuthorizedException;
}
$this->poll = $this->pollMapper->find($this->share->getPollId());
$this->validateShareAccess();
$this->request($permission);
} catch (DoesNotExistException $e) {
throw new NotAuthorizedException('Error loading share ' . $token);
}
return $this;
}
public function setPollId(?int $pollId = 0, string $permission = self::PERMISSION_POLL_VIEW): Acl {
try {
$this->poll = $this->pollMapper->find($pollId);
$this->request($permission);
} catch (DoesNotExistException $e) {
throw new NotAuthorizedException('Error loading poll ' . $pollId);
}
return $this;
}
public function getToken(): string {
return strval($this->share->getToken());
}
public function getPollId(): int {
return $this->poll->getId();
}
public function getPoll(): Poll {
return $this->poll;
}
public function getUserId(): string {
return $this->getIsLoggedIn() ? $this->userSession->getUser()->getUID() : $this->share->getUserId();
}
public function validateUserId(string $userId): void {
if ($this->getUserId() !== $userId) {
throw new NotAuthorizedException;
}
}
public function getIsOwner(): bool {
return ($this->getIsLoggedIn() && $this->poll->getOwner() === $this->getUserId());
}
private function getDisplayName(): string {
return $this->getIsLoggedIn() ? $this->userManager->get($this->getUserId())->getDisplayName() : $this->share->getDisplayName();
}
public function getIsAllowed(string $permission): bool {
switch ($permission) {
case self::PERMISSION_OVERRIDE:
return true;
case self::PERMISSION_POLL_VIEW:
if ($this->getIsAllowed(self::PERMISSION_POLL_EDIT)) {
return true; // always grant access, if user has edit rights
}
if ($this->poll->getDeleted()) {
return false; // always deny access, if poll is archived
}
if ($this->poll->getAccess() === Poll::ACCESS_OPEN) {
return true; // grant access if poll poll is public
}
if ($this->getIsInvolved()) {
return true; // grant access if user is involved in poll in any way
}
if ($this->getToken()) {
return true; // user has token
}
return false;
case self::PERMISSION_POLL_EDIT:
return $this->getIsOwner() || $this->getHasAdminAccess();
case self::PERMISSION_POLL_CREATE:
return $this->appSettings->getPollCreationAllowed();
case self::PERMISSION_POLL_MAILADDRESSES_VIEW:
return $this->appSettings->getAllowSeeMailAddresses();
case self::PERMISSION_POLL_DELETE:
return $this->getIsAllowed(self::PERMISSION_POLL_EDIT) || $this->getIsAdmin();
case self::PERMISSION_POLL_DOWNLOAD:
return $this->appSettings->getPollDownloadAllowed();
case self::PERMISSION_POLL_ARCHIVE:
return $this->getIsAllowed(self::PERMISSION_POLL_EDIT) || $this->getIsAdmin();
case self::PERMISSION_POLL_TAKEOVER:
return $this->getIsAdmin() && !$this->getIsOwner();
case self::PERMISSION_POLL_SUBSCRIBE:
return $this->getHasEmail();
case self::PERMISSION_POLL_RESULTS_VIEW:
return $this->getIsOwner()
|| $this->getIsDelegatedAdmin()
|| $this->poll->getShowResults() === Poll::SHOW_RESULTS_ALWAYS
|| $this->poll->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->poll->getExpired();
case self::PERMISSION_POLL_USERNAMES_VIEW:
return $this->getIsOwner() || $this->getIsDelegatedAdmin() || !$this->poll->getAnonymous();
case self::PERMISSION_OPTIONS_ADD:
return $this->getIsAllowed(self::PERMISSION_POLL_EDIT)
|| ($this->poll->getAllowProposals() === Poll::PROPOSAL_ALLOW
&& !$this->poll->getProposalsExpired()
&& $this->share->getType() !== Share::TYPE_PUBLIC);
case self::PERMISSION_COMMENT_ADD:
return $this->share->getType() !== Share::TYPE_PUBLIC && $this->poll->getallowComment();
case self::PERMISSION_VOTE_EDIT:
return !$this->poll->getExpired() && $this->share->getType() !== Share::TYPE_PUBLIC;
case self::PERMISSION_ALL_ACCESS:
return $this->appSettings->getAllAccessAllowed();
case self::PERMISSION_PUBLIC_SHARES:
return $this->appSettings->getPublicSharesAllowed();
}
return false;
}
public function request(string $permission): void {
if (!$this->getIsAllowed($permission)) {
throw new NotAuthorizedException('denied permission ' . $permission);
}
}
public function getIsVoteLimitExceeded(): bool {
// return true, if no vote limit is set
if ($this->getPoll()->getVoteLimit() < 1) {
return false;
}
// Only count votes, which match to an actual existing option.
// Explanation: If an option is deleted, the corresponding votes are not deleted.
$pollOptionTexts = array_map(function ($option) {
return $option->getPollOptionText();
}, $this->optionMapper->findByPoll($this->getPollId()));
$voteCount = 0;
$votes = $this->voteMapper->getYesVotesByParticipant($this->getPollId(), $this->getUserId());
foreach ($votes as $vote) {
if (in_array($vote->getVoteOptionText(), $pollOptionTexts)) {
$voteCount++;
}
}
if ($this->getPoll()->getVoteLimit() <= $voteCount) {
return true;
}
return false;
}
public function jsonSerialize(): array {
return [
'allowAddOptions' => $this->getIsAllowed(self::PERMISSION_OPTIONS_ADD),
'allowAllAccess' => $this->getIsAllowed(self::PERMISSION_ALL_ACCESS),
'allowArchive' => $this->getIsAllowed(self::PERMISSION_POLL_ARCHIVE),
'allowComment' => $this->getIsAllowed(self::PERMISSION_COMMENT_ADD),
'allowDelete' => $this->getIsAllowed(self::PERMISSION_POLL_DELETE),
'allowEdit' => $this->getIsAllowed(self::PERMISSION_POLL_EDIT),
'allowPollCreation' => $this->getIsAllowed(self::PERMISSION_POLL_CREATE),
'allowPollDownload' => $this->getIsAllowed(self::PERMISSION_POLL_DOWNLOAD),
'allowPublicShares' => $this->getIsAllowed(self::PERMISSION_PUBLIC_SHARES),
'allowSeeResults' => $this->getIsAllowed(self::PERMISSION_POLL_RESULTS_VIEW),
'allowSeeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW),
'allowSeeMailAddresses' => $this->getIsAllowed(self::PERMISSION_POLL_MAILADDRESSES_VIEW),
'allowSubscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE),
'allowView' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW),
'allowVote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT),
'displayName' => $this->getDisplayName(),
'isOwner' => $this->getIsOwner(),
'isVoteLimitExceeded' => $this->getIsVoteLimitExceeded(),
'loggedIn' => $this->getIsLoggedIn(),
'isNoUser' => !$this->getIsLoggedIn(),
'isGuest' => !$this->getIsLoggedIn(),
'pollId' => $this->getPollId(),
'token' => $this->getToken(),
'userHasVoted' => $this->getIsParticipant(),
'userId' => $this->getUserId(),
'userIsInvolved' => $this->getIsInvolved(),
'pollExpired' => $this->poll->getExpired(),
'pollExpire' => $this->poll->getExpire(),
];
}
/**
* getIsLogged - Is user logged in to nextcloud?
*/
public function getIsLoggedIn(): bool {
return $this->userSession->isLoggedIn();
}
/**
* getIsAdmin - Is the user admin
* Returns true, if user is in admin group
*/
private function getIsAdmin(): bool {
return ($this->getIsLoggedIn() && $this->groupManager->isAdmin($this->getUserId()));
}
private function getAllowSeeUserNames(): bool {
return ($this->getIsLoggedIn() && $this->groupManager->isAdmin($this->getUserId()));
}
/**
* getHasAdminAccess - Has user administrative rights?
* Returns true, if user is in admin group and poll has allowed admins to manage the poll,
* or when running console commands.
*/
private function getHasAdminAccess(): bool {
return (($this->getIsAdmin() && $this->poll->getAdminAccess())
|| defined('OC_CONSOLE')
|| $this->getIsDelegatedAdmin()
);
}
/**
* getIsInvolved - Is user involved?
* Returns true, if the current user is involved in the poll via share,
* as a participant or as the poll owner.
*/
private function getIsInvolved(): bool {
return (
$this->getIsOwner()
|| $this->getIsParticipant()
|| $this->getIsInvitedViaGroupShare()
|| $this->getIsPersonallyInvited());
}
/**
* getIsParticipant - Is user a participant?
* Returns true, if the current user is already a particitipant of the current poll.
*/
private function getIsParticipant(): bool {
return count(
$this->voteMapper->findParticipantsVotes($this->getPollId(), $this->getUserId())
) > 0;
}
/**
* getIsInvitedViaGroupShare - Is the poll shared via group share?
* Returns true, if the current poll contains a group share with a group,
* where the current user is member of. This only affects logged in users.
*/
private function getIsInvitedViaGroupShare(): bool {
if (!$this->getIsLoggedIn()) {
return false;
}
return 0 < count(
array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
return ($item->getType() === Share::TYPE_GROUP && $this->groupManager->isInGroup($this->getUserId(), $item->getUserId()));
})
);
}
/**
* getIsPersonallyInvited - Is the poll shared via user share?
* Returns true, if the current poll contains a user share for the current user.
* This only affects logged in users.
*/
private function getIsPersonallyInvited(): bool {
if (!$this->getIsLoggedIn()) {
return false;
}
return 0 < count(
array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
return ($item->getUserId() === $this->getUserId()
&& in_array($item->getType(), [
Share::TYPE_ADMIN,
Share::TYPE_USER,
Share::TYPE_EXTERNAL,
Share::TYPE_EMAIL,
Share::TYPE_CONTACT
])
);
})
);
}
/**
* getIsPersonallyInvited - Is the poll shared via user share?
* Returns true, if the current poll contains a user share for the current user.
* This only affects logged in users.
*/
private function getIsDelegatedAdmin(): bool {
if (!$this->getIsLoggedIn()) {
return false;
}
$filteredList = array_filter($this->shareMapper->findByPoll($this->getPollId()), function ($item) {
return ($item->getUserId() === $this->getUserId()
&& in_array($item->getType(), [
Share::TYPE_ADMIN,
])
);
});
return 0 < count($filteredList);
}
private function validateShareAccess(): void {
if ($this->getIsLoggedIn() && !$this->getIsShareValidForUsers()) {
throw new NotAuthorizedException('Share type "' . $this->share->getType() . '"only valid for guests');
}
if (!$this->getIsShareValidForGuests()) {
throw new NotAuthorizedException('Share type "' . $this->share->getType() . '"only valid for registered users');
};
}
private function getIsShareValidForGuests(): bool {
return in_array($this->share->getType(), [
Share::TYPE_PUBLIC,
Share::TYPE_EMAIL,
Share::TYPE_CONTACT,
Share::TYPE_EXTERNAL
]);
}
private function getIsShareValidForUsers(): bool {
return in_array($this->share->getType(), [
Share::TYPE_PUBLIC,
Share::TYPE_ADMIN,
Share::TYPE_USER,
Share::TYPE_GROUP
]);
}
private function getHasEmail(): bool {
return $this->share->getToken() ? strlen($this->share->getEmailAddress()) > 0 : $this->getIsLoggedIn();
}
}