Skip to content

Commit 94530a7

Browse files
committed
feat: add permissions for public link shares
Signed-off-by: Benjamin Frueh <benjamin.frueh@gmail.com>
1 parent 5339678 commit 94530a7

27 files changed

Lines changed: 955 additions & 145 deletions

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
['name' => 'share#show', 'url' => '/share/{id}', 'verb' => 'GET'],
9898
['name' => 'share#create', 'url' => '/share', 'verb' => 'POST'],
9999
['name' => 'share#updatePermission', 'url' => '/share/{id}/permission', 'verb' => 'PUT'],
100+
['name' => 'share#updatePermissions', 'url' => '/share/{id}/permissions', 'verb' => 'PUT'],
100101
['name' => 'share#updateDisplayMode', 'url' => '/share/{id}/display-mode', 'verb' => 'PUT'],
101102
['name' => 'share#destroy', 'url' => '/share/{id}', 'verb' => 'DELETE'],
102103

lib/Controller/Api1Controller.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ public function deleteShare(int $shareId): DataResponse {
672672
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
673673
public function updateSharePermissions(int $shareId, string $permissionType, bool $permissionValue): DataResponse {
674674
try {
675-
return new DataResponse($this->shareService->updatePermission($shareId, $permissionType, $permissionValue)->jsonSerialize());
675+
return new DataResponse($this->shareService->updatePermission($shareId, [$permissionType => $permissionValue])->jsonSerialize());
676676
} catch (PermissionError $e) {
677677
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
678678
$message = ['message' => $e->getMessage()];

lib/Controller/PublicRowOCSController.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
namespace OCA\Tables\Controller;
1010

1111
use OCA\Tables\AppInfo\Application;
12+
use OCA\Tables\Db\Row2Mapper;
1213
use OCA\Tables\Errors\BadRequestError;
1314
use OCA\Tables\Errors\InternalError;
1415
use OCA\Tables\Errors\NotFoundError;
1516
use OCA\Tables\Errors\PermissionError;
1617
use OCA\Tables\Helper\ConversionHelper;
1718
use OCA\Tables\Middleware\Attribute\AssertShareAccessIsAccessible;
19+
use OCA\Tables\Model\RowDataInput;
1820
use OCA\Tables\ResponseDefinitions;
1921
use OCA\Tables\Service\RowService;
2022
use OCA\Tables\Service\ShareService;
@@ -37,11 +39,13 @@ class PublicRowOCSController extends AOCSController {
3739
public function __construct(
3840
protected ShareService $shareService,
3941
protected RowService $rowService,
42+
protected Row2Mapper $row2Mapper,
4043
IRequest $request,
4144
LoggerInterface $logger,
4245
IL10N $l,
4346
) {
4447
parent::__construct($request, $logger, $l, '');
48+
$this->rowService->setPublicContext();
4549
}
4650

4751
/**
@@ -68,6 +72,10 @@ public function getRows(string $token, ?int $limit, ?int $offset): DataResponse
6872
$shareToken = new ShareToken($token);
6973
$share = $this->shareService->findByToken($shareToken);
7074

75+
if (!$share->getPermissionRead()) {
76+
return $this->handlePermissionError(new PermissionError('No read permission on this share'));
77+
}
78+
7179
$limit = $limit !== null ? max(0, min(500, $limit)) : null;
7280
$offset = $offset !== null ? max(0, $offset) : null;
7381

@@ -90,4 +98,153 @@ public function getRows(string $token, ?int $limit, ?int $offset): DataResponse
9098
return $this->handleBadRequestError($e);
9199
}
92100
}
101+
102+
/**
103+
* [api v2] Create a row in a link share
104+
*
105+
* @param string $token The share token
106+
* @param string|array<string, mixed> $data An array containing the column identifiers and their values
107+
* @return DataResponse<Http::STATUS_OK, TablesPublicRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
108+
*
109+
* 200: Row created
110+
* 400: Invalid request parameters
111+
* 403: No permissions
112+
* 404: Not found
113+
* 500: Internal error
114+
*/
115+
#[PublicPage]
116+
#[AssertShareAccessIsAccessible]
117+
#[ApiRoute(verb: 'POST', url: '/api/2/public/{token}/rows', requirements: ['token' => '[a-zA-Z0-9]{16}'])]
118+
#[OpenAPI]
119+
#[AnonRateLimit(limit: 20, period: 30)]
120+
public function createRow(string $token, mixed $data): DataResponse {
121+
try {
122+
$shareToken = new ShareToken($token);
123+
$share = $this->shareService->findByToken($shareToken);
124+
$this->row2Mapper->setUserId('public-' . $token);
125+
126+
if (!$share->getPermissionCreate()) {
127+
return $this->handlePermissionError(new PermissionError('No create permission on this share'));
128+
}
129+
130+
if (is_string($data)) {
131+
$data = json_decode($data, true);
132+
}
133+
if (!is_array($data)) {
134+
return $this->handleBadRequestError(new BadRequestError('Invalid data input'));
135+
}
136+
137+
$newRowData = new RowDataInput();
138+
foreach ($data as $key => $value) {
139+
$newRowData->add((int)$key, $value);
140+
}
141+
142+
$tableId = $share->getNodeType() === 'table' ? $share->getNodeId() : null;
143+
$viewId = $share->getNodeType() === 'view' ? $share->getNodeId() : null;
144+
145+
$row = $this->rowService->create($tableId, $viewId, $newRowData);
146+
return new DataResponse($this->rowService->formatRowsForPublicShare([$row])[0]);
147+
} catch (PermissionError $e) {
148+
return $this->handlePermissionError($e);
149+
} catch (NotFoundError $e) {
150+
return $this->handleNotFoundError($e);
151+
} catch (BadRequestError $e) {
152+
return $this->handleBadRequestError($e);
153+
} catch (InternalError|\Exception $e) {
154+
return $this->handleError($e);
155+
}
156+
}
157+
158+
/**
159+
* [api v2] Update a row in a link share
160+
*
161+
* @param string $token The share token
162+
* @param int $rowId The row identifier
163+
* @param string|array<string, mixed> $data An array containing the column identifiers and their values
164+
* @return DataResponse<Http::STATUS_OK, TablesPublicRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
165+
*
166+
* 200: Row updated
167+
* 400: Invalid request parameters
168+
* 403: No permissions
169+
* 404: Not found
170+
* 500: Internal error
171+
*/
172+
#[PublicPage]
173+
#[AssertShareAccessIsAccessible]
174+
#[ApiRoute(verb: 'PUT', url: '/api/2/public/{token}/rows/{rowId}', requirements: ['token' => '[a-zA-Z0-9]{16}', 'rowId' => '\d+'])]
175+
#[OpenAPI]
176+
#[AnonRateLimit(limit: 20, period: 30)]
177+
public function updateRow(string $token, int $rowId, mixed $data): DataResponse {
178+
try {
179+
$shareToken = new ShareToken($token);
180+
$share = $this->shareService->findByToken($shareToken);
181+
$this->row2Mapper->setUserId('public-' . $token);
182+
183+
if (!$share->getPermissionUpdate()) {
184+
return $this->handlePermissionError(new PermissionError('No update permission on this share'));
185+
}
186+
187+
if (is_string($data)) {
188+
$data = json_decode($data, true);
189+
}
190+
if (!is_array($data)) {
191+
return $this->handleBadRequestError(new BadRequestError('Invalid data input'));
192+
}
193+
194+
$viewId = $share->getNodeType() === 'view' ? $share->getNodeId() : null;
195+
$tableId = $share->getNodeType() === 'table' ? $share->getNodeId() : null;
196+
197+
$row = $this->rowService->updateSet($rowId, $viewId, $data, '', $tableId);
198+
return new DataResponse($this->rowService->formatRowsForPublicShare([$row])[0]);
199+
} catch (PermissionError $e) {
200+
return $this->handlePermissionError($e);
201+
} catch (NotFoundError $e) {
202+
return $this->handleNotFoundError($e);
203+
} catch (BadRequestError $e) {
204+
return $this->handleBadRequestError($e);
205+
} catch (InternalError|\Exception $e) {
206+
return $this->handleError($e);
207+
}
208+
}
209+
210+
/**
211+
* [api v2] Delete a row in a link share
212+
*
213+
* @param string $token The share token
214+
* @param int $rowId The row identifier
215+
* @return DataResponse<Http::STATUS_OK, TablesPublicRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
216+
*
217+
* 200: Row deleted
218+
* 403: No permissions
219+
* 404: Not found
220+
* 500: Internal error
221+
*/
222+
#[PublicPage]
223+
#[AssertShareAccessIsAccessible]
224+
#[ApiRoute(verb: 'DELETE', url: '/api/2/public/{token}/rows/{rowId}', requirements: ['token' => '[a-zA-Z0-9]{16}', 'rowId' => '\d+'])]
225+
#[OpenAPI]
226+
#[AnonRateLimit(limit: 20, period: 30)]
227+
public function deleteRow(string $token, int $rowId): DataResponse {
228+
try {
229+
$shareToken = new ShareToken($token);
230+
$share = $this->shareService->findByToken($shareToken);
231+
$this->row2Mapper->setUserId('public-' . $token);
232+
233+
if (!$share->getPermissionDelete()) {
234+
return $this->handlePermissionError(new PermissionError('No delete permission on this share'));
235+
}
236+
237+
$viewId = $share->getNodeType() === 'view' ? $share->getNodeId() : null;
238+
$tableId = $share->getNodeType() === 'table' ? $share->getNodeId() : null;
239+
240+
$row = $this->rowService->delete($rowId, $viewId, '', $tableId);
241+
return new DataResponse($this->rowService->formatRowsForPublicShare([$row])[0]);
242+
} catch (PermissionError $e) {
243+
return $this->handlePermissionError($e);
244+
} catch (NotFoundError $e) {
245+
return $this->handleNotFoundError($e);
246+
} catch (InternalError|\Exception $e) {
247+
return $this->handleError($e);
248+
}
249+
}
93250
}

lib/Controller/PublicSharePageController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ public function showShare(): TemplateResponse {
7171
$this->initialState->provideInitialState('shareToken', (string)$this->shareToken);
7272
$this->initialState->provideInitialState('nodeType', $this->share->getNodeType());
7373
$this->initialState->provideInitialState('nodeData', $nodeData);
74+
$this->initialState->provideInitialState('sharePermissions', [
75+
'read' => $this->share->getPermissionRead(),
76+
'create' => $this->share->getPermissionCreate(),
77+
'update' => $this->share->getPermissionUpdate(),
78+
'delete' => $this->share->getPermissionDelete(),
79+
]);
7480

7581
if (class_exists(LoadEditor::class)) {
7682
$this->eventDispatcher->dispatchTyped(new LoadEditor());

lib/Controller/ShareController.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,25 @@ public function create(
8585
#[NoAdminRequired]
8686
public function updatePermission(int $id, string $permission, bool $value): DataResponse {
8787
return $this->handleError(function () use ($id, $permission, $value) {
88-
return $this->service->updatePermission($id, $permission, $value);
88+
return $this->service->updatePermission($id, [$permission => $value]);
89+
});
90+
}
91+
92+
#[NoAdminRequired]
93+
public function updatePermissions(
94+
int $id,
95+
bool $permissionRead = false,
96+
bool $permissionCreate = false,
97+
bool $permissionUpdate = false,
98+
bool $permissionDelete = false,
99+
): DataResponse {
100+
return $this->handleError(function () use ($id, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete) {
101+
return $this->service->updatePermission($id, [
102+
'read' => $permissionRead,
103+
'create' => $permissionCreate,
104+
'update' => $permissionUpdate && $permissionRead,
105+
'delete' => $permissionDelete && $permissionRead,
106+
]);
89107
});
90108
}
91109

lib/Db/Row2Mapper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ public function getTableIdForRow(int $rowId): ?int {
122122
return $rowSleeve->getTableId();
123123
}
124124

125+
public function setUserId(string $userId): void {
126+
$this->userId = $userId;
127+
}
128+
125129
/**
126130
* @return int[]
127131
* @throws InternalError

lib/Service/PermissionsService.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class PermissionsService {
4545

4646
protected bool $isCli = false;
4747

48+
private bool $isPublicContext = false;
49+
4850
private ContextMapper $contextMapper;
4951

5052
public function __construct(
@@ -549,6 +551,11 @@ public function getPermissionArrayForNodeFromContexts(int $nodeId, string $nodeT
549551
);
550552
}
551553

554+
public function setPublicContext(): void {
555+
$this->userId = '';
556+
$this->isPublicContext = true;
557+
}
558+
552559
private function hasPermission(int $existingPermissions, string $permissionName): bool {
553560
$constantName = 'PERMISSION_' . strtoupper($permissionName);
554561
try {
@@ -634,7 +641,7 @@ private function basisCheck(Table|View|Context $element, string $nodeType, ?stri
634641
}
635642

636643
if ($userId === '') {
637-
return true;
644+
return $this->isCli || $this->isPublicContext;
638645
}
639646

640647
if ($this->userIsElementOwner($element, $userId, $nodeType)) {

lib/Service/RowService.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public function find(int $rowId): Row2 {
178178
* @throws InternalError
179179
*/
180180
public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): Row2 {
181-
if ($this->userId === null || $this->userId === '') {
181+
if ($this->userId === null) {
182182
$e = new \Exception('No user id in context, but needed.');
183183
$this->logger->error($e->getMessage(), ['exception' => $e]);
184184
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
@@ -523,10 +523,10 @@ private function getRowById(int $rowId): Row2 {
523523
}
524524
$row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId)));
525525
$row->markAsLoaded();
526-
} catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) {
526+
} catch (InternalError|MultipleObjectsReturnedException|Exception $e) {
527527
$this->logger->error($e->getMessage(), ['exception' => $e]);
528528
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
529-
} catch (NotFoundError $e) {
529+
} catch (NotFoundError|DoesNotExistException $e) {
530530
$this->logger->error($e->getMessage(), ['exception' => $e]);
531531
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
532532
}
@@ -677,7 +677,7 @@ public function updateSet(
677677
* @throws PermissionError
678678
* @noinspection DuplicatedCode
679679
*/
680-
public function delete(int $id, ?int $viewId, string $userId): Row2 {
680+
public function delete(int $id, ?int $viewId, string $userId, ?int $tableId = null): Row2 {
681681
try {
682682
$item = $this->getRowById($id);
683683
} catch (InternalError $e) {
@@ -688,6 +688,10 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 {
688688
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
689689
}
690690

691+
if ($tableId !== null && $tableId !== $item->getTableId()) {
692+
throw new NotFoundError('Row not found');
693+
}
694+
691695
if ($viewId) {
692696
// security
693697
if (!$this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {

0 commit comments

Comments
 (0)