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:
Ferdinand Thiessen 2023-01-22 19:52:07 +01:00
Родитель c271b25c28
Коммит 4211aa409f
6 изменённых файлов: 250 добавлений и 41 удалений

Просмотреть файл

@ -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;
}
});
}
} }