зеркало из 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',
|
||||
'requirements' => [
|
||||
'path' => '.+',
|
||||
'apiVersion' => 'v2'
|
||||
'apiVersion' => 'v2(\.1)?'
|
||||
]
|
||||
],
|
||||
|
||||
|
@ -219,6 +219,14 @@ return [
|
|||
'apiVersion' => 'v2'
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'shareApi#updateShare',
|
||||
'url' => '/api/{apiVersion}/share/update',
|
||||
'verb' => 'POST',
|
||||
'requirements' => [
|
||||
'apiVersion' => 'v2.1'
|
||||
]
|
||||
],
|
||||
|
||||
// Submissions
|
||||
[
|
||||
|
|
|
@ -855,8 +855,8 @@ class ApiController extends OCSController {
|
|||
throw new OCSBadRequestException();
|
||||
}
|
||||
|
||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
||||
$this->logger->debug('This form is not owned by the current user');
|
||||
if (!$this->formsService->canSeeResults($form->id)) {
|
||||
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||
throw new OCSForbiddenException();
|
||||
}
|
||||
|
||||
|
@ -1111,8 +1111,8 @@ class ApiController extends OCSController {
|
|||
throw new OCSBadRequestException();
|
||||
}
|
||||
|
||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
||||
$this->logger->debug('This form is not owned by the current user');
|
||||
if (!$this->formsService->canSeeResults($form->id)) {
|
||||
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||
throw new OCSForbiddenException();
|
||||
}
|
||||
|
||||
|
@ -1145,8 +1145,8 @@ class ApiController extends OCSController {
|
|||
throw new OCSBadRequestException();
|
||||
}
|
||||
|
||||
if ($form->getOwnerId() !== $this->currentUser->getUID()) {
|
||||
$this->logger->debug('This form is not owned by the current user');
|
||||
if (!$this->formsService->canSeeResults($form->id)) {
|
||||
$this->logger->debug('The current user has no permission to get the results for this form');
|
||||
throw new OCSForbiddenException();
|
||||
}
|
||||
|
||||
|
|
|
@ -120,11 +120,12 @@ class ShareApiController extends OCSController {
|
|||
* @throws OCSBadRequestException
|
||||
* @throws OCSForbiddenException
|
||||
*/
|
||||
public function newShare(int $formId, int $shareType, string $shareWith = ''): DataResponse {
|
||||
$this->logger->debug('Adding new share: formId: {formId}, shareType: {shareType}, shareWith: {shareWith}', [
|
||||
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}, permissions: {permissions}', [
|
||||
'formId' => $formId,
|
||||
'shareType' => $shareType,
|
||||
'shareWith' => $shareWith,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
// Only accept usable shareTypes
|
||||
|
@ -152,6 +153,10 @@ class ShareApiController extends OCSController {
|
|||
throw new OCSForbiddenException();
|
||||
}
|
||||
|
||||
if (!$this->validatePermissions($permissions, $shareType)) {
|
||||
throw new OCSBadRequestException('Invalid permission given');
|
||||
}
|
||||
|
||||
// Create public-share hash, if necessary.
|
||||
if ($shareType === IShare::TYPE_LINK) {
|
||||
$shareWith = $this->secureRandom->generate(
|
||||
|
@ -200,6 +205,7 @@ class ShareApiController extends OCSController {
|
|||
$share->setFormId($formId);
|
||||
$share->setShareType($shareType);
|
||||
$share->setShareWith($shareWith);
|
||||
$share->setPermissions($permissions);
|
||||
|
||||
$share = $this->shareMapper->insert($share);
|
||||
|
||||
|
@ -246,4 +252,95 @@ class ShareApiController extends OCSController {
|
|||
|
||||
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;
|
||||
|
||||
use OCA\Forms\Constants;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
|
@ -43,6 +44,8 @@ class Share extends Entity {
|
|||
protected $shareType;
|
||||
/** @var string */
|
||||
protected $shareWith;
|
||||
/** @var string */
|
||||
protected $permissionsJson;
|
||||
|
||||
/**
|
||||
* Option constructor.
|
||||
|
@ -53,12 +56,28 @@ class Share extends Entity {
|
|||
$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 {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'formId' => $this->getFormId(),
|
||||
'shareType' => (int)$this->getShareType(),
|
||||
'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.
|
||||
$result['canSubmit'] = $this->canSubmit($form->getId());
|
||||
|
||||
// Append submissionCount if currentUser is owner
|
||||
if ($this->currentUser && $form->getOwnerId() === $this->currentUser->getUID()) {
|
||||
// Append submissionCount if currentUser has permissions to see results
|
||||
if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) {
|
||||
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
||||
}
|
||||
|
||||
|
@ -230,8 +230,8 @@ class FormsService {
|
|||
'partial' => true
|
||||
];
|
||||
|
||||
// Append submissionCount if currentUser is owner
|
||||
if ($this->currentUser && $form->getOwnerId() === $this->currentUser->getUID()) {
|
||||
// Append submissionCount if currentUser has permissions to see results
|
||||
if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) {
|
||||
$result['submissionCount'] = $this->submissionMapper->countSubmissions($id);
|
||||
}
|
||||
|
||||
|
@ -271,12 +271,30 @@ class FormsService {
|
|||
}
|
||||
|
||||
$permissions = [];
|
||||
// Add submit permission if user has access.
|
||||
if ($this->hasUserAccess($formId)) {
|
||||
$permissions[] = Constants::PERMISSION_SUBMIT;
|
||||
$shares = $this->getSharesWithUser($formId, $this->currentUser->getUID());
|
||||
foreach ($shares as $share) {
|
||||
$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
|
||||
*
|
||||
* @param $formId
|
||||
* @param int $formId
|
||||
* @return bool
|
||||
*/
|
||||
public function isSharedToUser(int $formId): bool {
|
||||
$shareEntities = $this->shareMapper->findByForm($formId);
|
||||
foreach ($shareEntities as $shareEntity) {
|
||||
$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;
|
||||
$shareEntities = $this->getSharesWithUser($formId, $this->currentUser->getUID());
|
||||
return count($shareEntities) > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -495,4 +492,35 @@ class FormsService {
|
|||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче