зеркало из https://github.com/nextcloud/forms.git
Implement Share permissions and allow updating them
Allow users with `results` permission to query and export results Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
This commit is contained in:
Родитель
c271b25c28
Коммит
4211aa409f
|
@ -72,7 +72,7 @@ return [
|
||||||
'verb' => 'OPTIONS',
|
'verb' => 'OPTIONS',
|
||||||
'requirements' => [
|
'requirements' => [
|
||||||
'path' => '.+',
|
'path' => '.+',
|
||||||
'apiVersion' => 'v2'
|
'apiVersion' => 'v2(\.1)?'
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -219,6 +219,14 @@ return [
|
||||||
'apiVersion' => 'v2'
|
'apiVersion' => 'v2'
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'shareApi#updateShare',
|
||||||
|
'url' => '/api/{apiVersion}/share/update',
|
||||||
|
'verb' => 'POST',
|
||||||
|
'requirements' => [
|
||||||
|
'apiVersion' => 'v2.1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
|
||||||
// Submissions
|
// Submissions
|
||||||
[
|
[
|
||||||
|
|
|
@ -855,8 +855,8 @@ class ApiController extends OCSController {
|
||||||
throw new OCSBadRequestException();
|
throw new OCSBadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
if (!$this->formsService->canSeeResults($form->id)) {
|
||||||
$this->logger->debug('This form is not owned by the current user');
|
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||||
throw new OCSForbiddenException();
|
throw new OCSForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1111,8 +1111,8 @@ class ApiController extends OCSController {
|
||||||
throw new OCSBadRequestException();
|
throw new OCSBadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
if (!$this->formsService->canSeeResults($form->id)) {
|
||||||
$this->logger->debug('This form is not owned by the current user');
|
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||||
throw new OCSForbiddenException();
|
throw new OCSForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1145,8 +1145,8 @@ class ApiController extends OCSController {
|
||||||
throw new OCSBadRequestException();
|
throw new OCSBadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
if (!$this->formsService->canSeeResults($form->id)) {
|
||||||
$this->logger->debug('This form is not owned by the current user');
|
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||||
throw new OCSForbiddenException();
|
throw new OCSForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,11 +120,12 @@ class ShareApiController extends OCSController {
|
||||||
* @throws OCSBadRequestException
|
* @throws OCSBadRequestException
|
||||||
* @throws OCSForbiddenException
|
* @throws OCSForbiddenException
|
||||||
*/
|
*/
|
||||||
public function newShare(int $formId, int $shareType, string $shareWith = ''): DataResponse {
|
public function newShare(int $formId, int $shareType, string $shareWith = '', array $permissions = [Constants::PERMISSION_SUBMIT]): DataResponse {
|
||||||
$this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}', [
|
$this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}, permissions: {permissions}', [
|
||||||
'formId' => $formId,
|
'formId' => $formId,
|
||||||
'shareType' => $shareType,
|
'shareType' => $shareType,
|
||||||
'shareWith' => $shareWith,
|
'shareWith' => $shareWith,
|
||||||
|
'permissions' => $permissions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Only accept usable shareTypes
|
// Only accept usable shareTypes
|
||||||
|
@ -152,6 +153,10 @@ class ShareApiController extends OCSController {
|
||||||
throw new OCSForbiddenException();
|
throw new OCSForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->validatePermissions($permissions, $shareType)) {
|
||||||
|
throw new OCSBadRequestException('Invalid permission given');
|
||||||
|
}
|
||||||
|
|
||||||
// Create public-share hash, if necessary.
|
// Create public-share hash, if necessary.
|
||||||
if ($shareType === IShare::TYPE_LINK) {
|
if ($shareType === IShare::TYPE_LINK) {
|
||||||
$shareWith = $this->secureRandom->generate(
|
$shareWith = $this->secureRandom->generate(
|
||||||
|
@ -200,6 +205,7 @@ class ShareApiController extends OCSController {
|
||||||
$share->setFormId($formId);
|
$share->setFormId($formId);
|
||||||
$share->setShareType($shareType);
|
$share->setShareType($shareType);
|
||||||
$share->setShareWith($shareWith);
|
$share->setShareWith($shareWith);
|
||||||
|
$share->setPermissions($permissions);
|
||||||
|
|
||||||
$share = $this->shareMapper->insert($share);
|
$share = $this->shareMapper->insert($share);
|
||||||
|
|
||||||
|
@ -246,4 +252,95 @@ class ShareApiController extends OCSController {
|
||||||
|
|
||||||
return new DataResponse($id);
|
return new DataResponse($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @CORS
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* Update permissions of a share
|
||||||
|
*
|
||||||
|
* @param int $id of the share to update
|
||||||
|
* @param array $keyValuePairs Array of key=>value pairs to update.
|
||||||
|
* @return DataResponse
|
||||||
|
* @throws OCSBadRequestException
|
||||||
|
* @throws OCSForbiddenException
|
||||||
|
*/
|
||||||
|
public function updateShare(int $id, array $keyValuePairs): DataResponse {
|
||||||
|
$this->logger->debug('Updating share: {id}, permissions: {permissions}', [
|
||||||
|
'id' => $id,
|
||||||
|
'keyValuePairs' => $keyValuePairs
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$share = $this->shareMapper->findById($id);
|
||||||
|
$form = $this->formMapper->findById($share->getFormId());
|
||||||
|
} catch (IMapperException $e) {
|
||||||
|
$this->logger->debug('Could not find share', ['exception' => $e]);
|
||||||
|
throw new OCSBadRequestException('Could not find share');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
||||||
|
$this->logger->debug('This form is not owned by the current user');
|
||||||
|
throw new OCSForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow empty array
|
||||||
|
if (sizeof($keyValuePairs) === 0) {
|
||||||
|
$this->logger->info('Empty keyValuePairs, will not update.');
|
||||||
|
throw new OCSForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Don't allow to change other properties than permissions
|
||||||
|
if (count($keyValuePairs) > 1 || !key_exists('permissions', $keyValuePairs)) {
|
||||||
|
$this->logger->debug('Not allowed to update other properties than permissions');
|
||||||
|
throw new OCSForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->validatePermissions($keyValuePairs['permissions'], $share->getShareType())) {
|
||||||
|
throw new OCSBadRequestException('Invalid permission given');
|
||||||
|
}
|
||||||
|
|
||||||
|
$share->setPermissions($keyValuePairs['permissions']);
|
||||||
|
$share = $this->shareMapper->update($share);
|
||||||
|
|
||||||
|
return new DataResponse($share->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate user given permission array
|
||||||
|
*
|
||||||
|
* @param array $permissions User given permissions
|
||||||
|
* @return array Sanitized array of permissions
|
||||||
|
* @throws OCSBadRequestException If invalid permission was given
|
||||||
|
*/
|
||||||
|
protected function validatePermissions(array $permissions, int $shareType): bool {
|
||||||
|
if (count($permissions) === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sanitizedPermissions = array_intersect(Constants::PERMISSION_ALL, $permissions);
|
||||||
|
if (count($sanitizedPermissions) < count($permissions)) {
|
||||||
|
$this->logger->debug('Invalid permission given', ['invalid_permissions' => array_diff($permissions, $sanitizedPermissions)]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array(Constants::PERMISSION_SUBMIT, $sanitizedPermissions)) {
|
||||||
|
$this->logger->debug('Submit permission must always be granted');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure only users can have special permissions
|
||||||
|
if (count($sanitizedPermissions) > 1) {
|
||||||
|
switch ($shareType) {
|
||||||
|
case IShare::TYPE_USER:
|
||||||
|
case IShare::TYPE_GROUP:
|
||||||
|
case IShare::TYPE_CIRCLE:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// e.g. link shares ...
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Forms\Db;
|
namespace OCA\Forms\Db;
|
||||||
|
|
||||||
|
use OCA\Forms\Constants;
|
||||||
use OCP\AppFramework\Db\Entity;
|
use OCP\AppFramework\Db\Entity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +44,8 @@ class Share extends Entity {
|
||||||
protected $shareType;
|
protected $shareType;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $shareWith;
|
protected $shareWith;
|
||||||
|
/** @var string */
|
||||||
|
protected $permissionsJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option constructor.
|
* Option constructor.
|
||||||
|
@ -53,12 +56,28 @@ class Share extends Entity {
|
||||||
$this->addType('shareWith', 'string');
|
$this->addType('shareWith', 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPermissions(): array {
|
||||||
|
// Fallback to submit permission
|
||||||
|
return json_decode($this->getPermissionsJson() ?: 'null') ?? [ Constants::PERMISSION_SUBMIT ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $permissions
|
||||||
|
*/
|
||||||
|
public function setPermissions(array $permissions) {
|
||||||
|
$this->setPermissionsJson(
|
||||||
|
// Make sure to only encode array values as the indices might be non consecutively so it would be encoded as a json object
|
||||||
|
json_encode(array_values($permissions))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function read(): array {
|
public function read(): array {
|
||||||
return [
|
return [
|
||||||
'id' => $this->getId(),
|
'id' => $this->getId(),
|
||||||
'formId' => $this->getFormId(),
|
'formId' => $this->getFormId(),
|
||||||
'shareType' => (int)$this->getShareType(),
|
'shareType' => (int)$this->getShareType(),
|
||||||
'shareWith' => $this->getShareWith(),
|
'shareWith' => $this->getShareWith(),
|
||||||
|
'permissions' => $this->getPermissions(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2023 Ferdinand Thiessen <rpm@fthiessen.de>
|
||||||
|
*
|
||||||
|
* @author Ferdinand Thiessen <rpm@fthiessen.de>
|
||||||
|
*
|
||||||
|
* @license AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* 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\Forms\Migration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
use OCP\DB\Types;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\SimpleMigrationStep;
|
||||||
|
|
||||||
|
class Version030100Date20230123182700 extends SimpleMigrationStep {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOutput $output
|
||||||
|
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||||
|
* @param array $options
|
||||||
|
* @return null|ISchemaWrapper
|
||||||
|
*/
|
||||||
|
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||||
|
/** @var ISchemaWrapper $schema */
|
||||||
|
$schema = $schemaClosure();
|
||||||
|
$table = $schema->getTable('forms_v2_shares');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('permissions_json')) {
|
||||||
|
$table->addColumn('permissions_json', Types::JSON, [
|
||||||
|
'notnull' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -203,8 +203,8 @@ class FormsService {
|
||||||
// Append canSubmit, to be able to show proper EmptyContent on internal view.
|
// Append canSubmit, to be able to show proper EmptyContent on internal view.
|
||||||
$result['canSubmit'] = $this->canSubmit($form->getId());
|
$result['canSubmit'] = $this->canSubmit($form->getId());
|
||||||
|
|
||||||
// Append submissionCount if currentUser is owner
|
// Append submissionCount if currentUser has permissions to see results
|
||||||
if ($this->currentUser && $form->getOwnerId() === $this->currentUser->getUID()) {
|
if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) {
|
||||||
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,8 +230,8 @@ class FormsService {
|
||||||
'partial' => true
|
'partial' => true
|
||||||
];
|
];
|
||||||
|
|
||||||
// Append submissionCount if currentUser is owner
|
// Append submissionCount if currentUser has permissions to see results
|
||||||
if ($this->currentUser && $form->getOwnerId() === $this->currentUser->getUID()) {
|
if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) {
|
||||||
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,12 +271,30 @@ class FormsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
$permissions = [];
|
$permissions = [];
|
||||||
// Add submit permission if user has access.
|
$shares = $this->getSharesWithUser($formId, $this->currentUser->getUID());
|
||||||
if ($this->hasUserAccess($formId)) {
|
foreach ($shares as $share) {
|
||||||
$permissions[] = Constants::PERMISSION_SUBMIT;
|
$permissions = array_merge($permissions, $share->getPermissions());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $permissions;
|
// Fall back to submit permission if access is granted to all users
|
||||||
|
if (count($permissions) === 0) {
|
||||||
|
$access = $form->getAccess();
|
||||||
|
if ($access['permitAllUsers'] && $this->configService->getAllowPermitAll()) {
|
||||||
|
$permissions = [Constants::PERMISSION_SUBMIT];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can the current user see results of a form
|
||||||
|
*
|
||||||
|
* @param int $formId
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function canSeeResults(int $formId): bool {
|
||||||
|
return in_array(Constants::PERMISSION_RESULTS, $this->getPermissions($formId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -409,33 +427,12 @@ class FormsService {
|
||||||
/**
|
/**
|
||||||
* Checking all selected shares
|
* Checking all selected shares
|
||||||
*
|
*
|
||||||
* @param $formId
|
* @param int $formId
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isSharedToUser(int $formId): bool {
|
public function isSharedToUser(int $formId): bool {
|
||||||
$shareEntities = $this->shareMapper->findByForm($formId);
|
$shareEntities = $this->getSharesWithUser($formId, $this->currentUser->getUID());
|
||||||
foreach ($shareEntities as $shareEntity) {
|
return count($shareEntities) > 0;
|
||||||
$share = $shareEntity->read();
|
|
||||||
|
|
||||||
// Needs different handling for shareTypes
|
|
||||||
switch ($share['shareType']) {
|
|
||||||
case IShare::TYPE_USER:
|
|
||||||
if ($share['shareWith'] === $this->currentUser->getUID()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IShare::TYPE_GROUP:
|
|
||||||
if ($this->groupManager->isInGroup($this->currentUser->getUID(), $share['shareWith'])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Return false below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No share found.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -495,4 +492,35 @@ class FormsService {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return shares of a form shared with given user
|
||||||
|
*
|
||||||
|
* @param int $formId The form to query shares for
|
||||||
|
* @param string $userId The user to check if shared with
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getSharesWithUser(int $formId, string $userId): array {
|
||||||
|
$shareEntities = $this->shareMapper->findByForm($formId);
|
||||||
|
|
||||||
|
return array_filter($shareEntities, function ($shareEntity) use ($userId) {
|
||||||
|
$share = $shareEntity->read();
|
||||||
|
|
||||||
|
// Needs different handling for shareTypes
|
||||||
|
switch ($share['shareType']) {
|
||||||
|
case IShare::TYPE_USER:
|
||||||
|
if ($share['shareWith'] === $userId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IShare::TYPE_GROUP:
|
||||||
|
if ($this->groupManager->isInGroup($userId, $share['shareWith'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче